diff options
Diffstat (limited to 'src/com/android/inputmethod/pinyin')
22 files changed, 9314 insertions, 0 deletions
diff --git a/src/com/android/inputmethod/pinyin/BalloonHint.java b/src/com/android/inputmethod/pinyin/BalloonHint.java new file mode 100644 index 0000000..919dbf1 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/BalloonHint.java @@ -0,0 +1,472 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Paint.FontMetricsInt; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.view.Gravity; +import android.view.View; +import android.view.View.MeasureSpec; +import android.widget.PopupWindow; + +/** + * Subclass of PopupWindow used as the feedback when user presses on a soft key + * or a candidate. + */ +public class BalloonHint extends PopupWindow { + /** + * Delayed time to show the balloon hint. + */ + public static final int TIME_DELAY_SHOW = 0; + + /** + * Delayed time to dismiss the balloon hint. + */ + public static final int TIME_DELAY_DISMISS = 200; + + /** + * The padding information of the balloon. Because PopupWindow's background + * can not be changed unless it is dismissed and shown again, we set the + * real background drawable to the content view, and make the PopupWindow's + * background transparent. So actually this padding information is for the + * content view. + */ + private Rect mPaddingRect = new Rect(); + + /** + * The context used to create this balloon hint object. + */ + private Context mContext; + + /** + * Parent used to show the balloon window. + */ + private View mParent; + + /** + * The content view of the balloon. + */ + BalloonView mBalloonView; + + /** + * The measuring specification used to determine its size. Key-press + * balloons and candidates balloons have different measuring specifications. + */ + private int mMeasureSpecMode; + + /** + * Used to indicate whether the balloon needs to be dismissed forcibly. + */ + private boolean mForceDismiss; + + /** + * Timer used to show/dismiss the balloon window with some time delay. + */ + private BalloonTimer mBalloonTimer; + + private int mParentLocationInWindow[] = new int[2]; + + public BalloonHint(Context context, View parent, int measureSpecMode) { + super(context); + mParent = parent; + mMeasureSpecMode = measureSpecMode; + + setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + setTouchable(false); + setBackgroundDrawable(new ColorDrawable(0)); + + mBalloonView = new BalloonView(context); + mBalloonView.setClickable(false); + setContentView(mBalloonView); + + mBalloonTimer = new BalloonTimer(); + } + + public Context getContext() { + return mContext; + } + + public Rect getPadding() { + return mPaddingRect; + } + + public void setBalloonBackground(Drawable drawable) { + // We usually pick up a background from a soft keyboard template, + // and the object may has been set to this balloon before. + if (mBalloonView.getBackground() == drawable) return; + mBalloonView.setBackgroundDrawable(drawable); + + if (null != drawable) { + drawable.getPadding(mPaddingRect); + } else { + mPaddingRect.set(0, 0, 0, 0); + } + } + + /** + * Set configurations to show text label in this balloon. + * + * @param label The text label to show in the balloon. + * @param textSize The text size used to show label. + * @param textBold Used to indicate whether the label should be bold. + * @param textColor The text color used to show label. + * @param width The desired width of the balloon. The real width is + * determined by the desired width and balloon's measuring + * specification. + * @param height The desired width of the balloon. The real width is + * determined by the desired width and balloon's measuring + * specification. + */ + public void setBalloonConfig(String label, float textSize, + boolean textBold, int textColor, int width, int height) { + mBalloonView.setTextConfig(label, textSize, textBold, textColor); + setBalloonSize(width, height); + } + + /** + * Set configurations to show text label in this balloon. + * + * @param icon The icon used to shown in this balloon. + * @param width The desired width of the balloon. The real width is + * determined by the desired width and balloon's measuring + * specification. + * @param height The desired width of the balloon. The real width is + * determined by the desired width and balloon's measuring + * specification. + */ + public void setBalloonConfig(Drawable icon, int width, int height) { + mBalloonView.setIcon(icon); + setBalloonSize(width, height); + } + + + public boolean needForceDismiss() { + return mForceDismiss; + } + + public int getPaddingLeft() { + return mPaddingRect.left; + } + + public int getPaddingTop() { + return mPaddingRect.top; + } + + public int getPaddingRight() { + return mPaddingRect.right; + } + + public int getPaddingBottom() { + return mPaddingRect.bottom; + } + + public void delayedShow(long delay, int locationInParent[]) { + if (mBalloonTimer.isPending()) { + mBalloonTimer.removeTimer(); + } + if (delay <= 0) { + mParent.getLocationInWindow(mParentLocationInWindow); + showAtLocation(mParent, Gravity.LEFT | Gravity.TOP, + locationInParent[0], locationInParent[1] + + mParentLocationInWindow[1]); + } else { + mBalloonTimer.startTimer(delay, BalloonTimer.ACTION_SHOW, + locationInParent, -1, -1); + } + } + + public void delayedUpdate(long delay, int locationInParent[], + int width, int height) { + mBalloonView.invalidate(); + if (mBalloonTimer.isPending()) { + mBalloonTimer.removeTimer(); + } + if (delay <= 0) { + mParent.getLocationInWindow(mParentLocationInWindow); + update(locationInParent[0], locationInParent[1] + + mParentLocationInWindow[1], width, height); + } else { + mBalloonTimer.startTimer(delay, BalloonTimer.ACTION_UPDATE, + locationInParent, width, height); + } + } + + public void delayedDismiss(long delay) { + if (mBalloonTimer.isPending()) { + mBalloonTimer.removeTimer(); + int pendingAction = mBalloonTimer.getAction(); + if (0 != delay && BalloonTimer.ACTION_HIDE != pendingAction) { + mBalloonTimer.run(); + } + } + if (delay <= 0) { + dismiss(); + } else { + mBalloonTimer.startTimer(delay, BalloonTimer.ACTION_HIDE, null, -1, + -1); + } + } + + public void removeTimer() { + if (mBalloonTimer.isPending()) { + mBalloonTimer.removeTimer(); + } + } + + private void setBalloonSize(int width, int height) { + int widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, + mMeasureSpecMode); + int heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, + mMeasureSpecMode); + mBalloonView.measure(widthMeasureSpec, heightMeasureSpec); + + int oldWidth = getWidth(); + int oldHeight = getHeight(); + int newWidth = mBalloonView.getMeasuredWidth() + getPaddingLeft() + + getPaddingRight(); + int newHeight = mBalloonView.getMeasuredHeight() + getPaddingTop() + + getPaddingBottom(); + setWidth(newWidth); + setHeight(newHeight); + + // If update() is called to update both size and position, the system + // will first MOVE the PopupWindow to the new position, and then + // perform a size-updating operation, so there will be a flash in + // PopupWindow if user presses a key and moves finger to next one whose + // size is different. + // PopupWindow will handle the updating issue in one go in the future, + // but before that, if we find the size is changed, a mandatory dismiss + // operation is required. In our UI design, normal QWERTY keys' width + // can be different in 1-pixel, and we do not dismiss the balloon when + // user move between QWERTY keys. + mForceDismiss = false; + if (isShowing()) { + mForceDismiss = oldWidth - newWidth > 1 || newWidth - oldWidth > 1; + } + } + + + private class BalloonTimer extends Handler implements Runnable { + public static final int ACTION_SHOW = 1; + public static final int ACTION_HIDE = 2; + public static final int ACTION_UPDATE = 3; + + /** + * The pending action. + */ + private int mAction; + + private int mPositionInParent[] = new int[2]; + private int mWidth; + private int mHeight; + + private boolean mTimerPending = false; + + public void startTimer(long time, int action, int positionInParent[], + int width, int height) { + mAction = action; + if (ACTION_HIDE != action) { + mPositionInParent[0] = positionInParent[0]; + mPositionInParent[1] = positionInParent[1]; + } + mWidth = width; + mHeight = height; + postDelayed(this, time); + mTimerPending = true; + } + + public boolean isPending() { + return mTimerPending; + } + + public boolean removeTimer() { + if (mTimerPending) { + mTimerPending = false; + removeCallbacks(this); + return true; + } + + return false; + } + + public int getAction() { + return mAction; + } + + public void run() { + switch (mAction) { + case ACTION_SHOW: + mParent.getLocationInWindow(mParentLocationInWindow); + showAtLocation(mParent, Gravity.LEFT | Gravity.TOP, + mPositionInParent[0], mPositionInParent[1] + + mParentLocationInWindow[1]); + break; + case ACTION_HIDE: + dismiss(); + break; + case ACTION_UPDATE: + mParent.getLocationInWindow(mParentLocationInWindow); + update(mPositionInParent[0], mPositionInParent[1] + + mParentLocationInWindow[1], mWidth, mHeight); + } + mTimerPending = false; + } + } + + private class BalloonView extends View { + /** + * Suspension points used to display long items. + */ + private static final String SUSPENSION_POINTS = "..."; + + /** + * The icon to be shown. If it is not null, {@link #mLabel} will be + * ignored. + */ + private Drawable mIcon; + + /** + * The label to be shown. It is enabled only if {@link #mIcon} is null. + */ + private String mLabel; + + private int mLabeColor = 0xff000000; + private Paint mPaintLabel; + private FontMetricsInt mFmi; + + /** + * The width to show suspension points. + */ + private float mSuspensionPointsWidth; + + + public BalloonView(Context context) { + super(context); + mPaintLabel = new Paint(); + mPaintLabel.setColor(mLabeColor); + mPaintLabel.setAntiAlias(true); + mPaintLabel.setFakeBoldText(true); + mFmi = mPaintLabel.getFontMetricsInt(); + } + + public void setIcon(Drawable icon) { + mIcon = icon; + } + + public void setTextConfig(String label, float fontSize, + boolean textBold, int textColor) { + // Icon should be cleared so that the label will be enabled. + mIcon = null; + mLabel = label; + mPaintLabel.setTextSize(fontSize); + mPaintLabel.setFakeBoldText(textBold); + mPaintLabel.setColor(textColor); + mFmi = mPaintLabel.getFontMetricsInt(); + mSuspensionPointsWidth = mPaintLabel.measureText(SUSPENSION_POINTS); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode == MeasureSpec.EXACTLY) { + setMeasuredDimension(widthSize, heightSize); + return; + } + + int measuredWidth = mPaddingLeft + mPaddingRight; + int measuredHeight = mPaddingTop + mPaddingBottom; + if (null != mIcon) { + measuredWidth += mIcon.getIntrinsicWidth(); + measuredHeight += mIcon.getIntrinsicHeight(); + } else if (null != mLabel) { + measuredWidth += (int) (mPaintLabel.measureText(mLabel)); + measuredHeight += mFmi.bottom - mFmi.top; + } + if (widthSize > measuredWidth || widthMode == MeasureSpec.AT_MOST) { + measuredWidth = widthSize; + } + + if (heightSize > measuredHeight + || heightMode == MeasureSpec.AT_MOST) { + measuredHeight = heightSize; + } + + int maxWidth = Environment.getInstance().getScreenWidth() - + mPaddingLeft - mPaddingRight; + if (measuredWidth > maxWidth) { + measuredWidth = maxWidth; + } + setMeasuredDimension(measuredWidth, measuredHeight); + } + + @Override + protected void onDraw(Canvas canvas) { + int width = getWidth(); + int height = getHeight(); + if (null != mIcon) { + int marginLeft = (width - mIcon.getIntrinsicWidth()) / 2; + int marginRight = width - mIcon.getIntrinsicWidth() + - marginLeft; + int marginTop = (height - mIcon.getIntrinsicHeight()) / 2; + int marginBottom = height - mIcon.getIntrinsicHeight() + - marginTop; + mIcon.setBounds(marginLeft, marginTop, width - marginRight, + height - marginBottom); + mIcon.draw(canvas); + } else if (null != mLabel) { + float labelMeasuredWidth = mPaintLabel.measureText(mLabel); + float x = mPaddingLeft; + x += (width - labelMeasuredWidth - mPaddingLeft - mPaddingRight) / 2.0f; + String labelToDraw = mLabel; + if (x < mPaddingLeft) { + x = mPaddingLeft; + labelToDraw = getLimitedLabelForDrawing(mLabel, + width - mPaddingLeft - mPaddingRight); + } + + int fontHeight = mFmi.bottom - mFmi.top; + float marginY = (height - fontHeight) / 2.0f; + float y = marginY - mFmi.top; + canvas.drawText(labelToDraw, x, y, mPaintLabel); + } + } + + private String getLimitedLabelForDrawing(String rawLabel, + float widthToDraw) { + int subLen = rawLabel.length(); + if (subLen <= 1) return rawLabel; + do { + subLen--; + float width = mPaintLabel.measureText(rawLabel, 0, subLen); + if (width + mSuspensionPointsWidth <= widthToDraw || 1 >= subLen) { + return rawLabel.substring(0, subLen) + + SUSPENSION_POINTS; + } + } while (true); + } + } +} diff --git a/src/com/android/inputmethod/pinyin/CandidateView.java b/src/com/android/inputmethod/pinyin/CandidateView.java new file mode 100644 index 0000000..8dc1bf1 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/CandidateView.java @@ -0,0 +1,760 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import com.android.inputmethod.pinyin.PinyinIME.DecodingInfo; + +import java.util.Vector; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Paint.FontMetricsInt; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; + +/** + * View to show candidate list. There two candidate view instances which are + * used to show animation when user navigates between pages. + */ +public class CandidateView extends View { + /** + * The minimum width to show a item. + */ + private static final float MIN_ITEM_WIDTH = 22; + + /** + * Suspension points used to display long items. + */ + private static final String SUSPENSION_POINTS = "..."; + + /** + * The width to draw candidates. + */ + private int mContentWidth; + + /** + * The height to draw candidate content. + */ + private int mContentHeight; + + /** + * Whether footnotes are displayed. Footnote is shown when hardware keyboard + * is available. + */ + private boolean mShowFootnote = true; + + /** + * Balloon hint for candidate press/release. + */ + private BalloonHint mBalloonHint; + + /** + * Desired position of the balloon to the input view. + */ + private int mHintPositionToInputView[] = new int[2]; + + /** + * Decoding result to show. + */ + private DecodingInfo mDecInfo; + + /** + * Listener used to notify IME that user clicks a candidate, or navigate + * between them. + */ + private CandidateViewListener mCvListener; + + /** + * Used to notify the container to update the status of forward/backward + * arrows. + */ + private ArrowUpdater mArrowUpdater; + + /** + * If true, update the arrow status when drawing candidates. + */ + private boolean mUpdateArrowStatusWhenDraw = false; + + /** + * Page number of the page displayed in this view. + */ + private int mPageNo; + + /** + * Active candidate position in this page. + */ + private int mActiveCandInPage; + + /** + * Used to decided whether the active candidate should be highlighted or + * not. If user changes focus to composing view (The view to show Pinyin + * string), the highlight in candidate view should be removed. + */ + private boolean mEnableActiveHighlight = true; + + /** + * The page which is just calculated. + */ + private int mPageNoCalculated = -1; + + /** + * The Drawable used to display as the background of the high-lighted item. + */ + private Drawable mActiveCellDrawable; + + /** + * The Drawable used to display as separators between candidates. + */ + private Drawable mSeparatorDrawable; + + /** + * Color to draw normal candidates generated by IME. + */ + private int mImeCandidateColor; + + /** + * Color to draw normal candidates Recommended by application. + */ + private int mRecommendedCandidateColor; + + /** + * Color to draw the normal(not highlighted) candidates, it can be one of + * {@link #mImeCandidateColor} or {@link #mRecommendedCandidateColor}. + */ + private int mNormalCandidateColor; + + /** + * Color to draw the active(highlighted) candidates, including candidates + * from IME and candidates from application. + */ + private int mActiveCandidateColor; + + /** + * Text size to draw candidates generated by IME. + */ + private int mImeCandidateTextSize; + + /** + * Text size to draw candidates recommended by application. + */ + private int mRecommendedCandidateTextSize; + + /** + * The current text size to draw candidates. It can be one of + * {@link #mImeCandidateTextSize} or {@link #mRecommendedCandidateTextSize}. + */ + private int mCandidateTextSize; + + /** + * Paint used to draw candidates. + */ + private Paint mCandidatesPaint; + + /** + * Used to draw footnote. + */ + private Paint mFootnotePaint; + + /** + * The width to show suspension points. + */ + private float mSuspensionPointsWidth; + + /** + * Rectangle used to draw the active candidate. + */ + private RectF mActiveCellRect; + + /** + * Left and right margins for a candidate. It is specified in xml, and is + * the minimum margin for a candidate. The actual gap between two candidates + * is 2 * {@link #mCandidateMargin} + {@link #mSeparatorDrawable}. + * getIntrinsicWidth(). Because length of candidate is not fixed, there can + * be some extra space after the last candidate in the current page. In + * order to achieve best look-and-feel, this extra space will be divided and + * allocated to each candidates. + */ + private float mCandidateMargin; + + /** + * Left and right extra margins for a candidate. + */ + private float mCandidateMarginExtra; + + /** + * Rectangles for the candidates in this page. + **/ + private Vector<RectF> mCandRects; + + /** + * FontMetricsInt used to measure the size of candidates. + */ + private FontMetricsInt mFmiCandidates; + + /** + * FontMetricsInt used to measure the size of footnotes. + */ + private FontMetricsInt mFmiFootnote; + + private PressTimer mTimer = new PressTimer(); + + private GestureDetector mGestureDetector; + + private int mLocationTmp[] = new int[2]; + + public CandidateView(Context context, AttributeSet attrs) { + super(context, attrs); + + Resources r = context.getResources(); + + Configuration conf = r.getConfiguration(); + if (conf.keyboard == Configuration.KEYBOARD_NOKEYS + || conf.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) { + mShowFootnote = false; + } + + mActiveCellDrawable = r.getDrawable(R.drawable.candidate_hl_bg); + mSeparatorDrawable = r.getDrawable(R.drawable.candidates_vertical_line); + mCandidateMargin = r.getDimension(R.dimen.candidate_margin_left_right); + + mImeCandidateColor = r.getColor(R.color.candidate_color); + mRecommendedCandidateColor = r.getColor(R.color.recommended_candidate_color); + mNormalCandidateColor = mImeCandidateColor; + mActiveCandidateColor = r.getColor(R.color.active_candidate_color); + + mCandidatesPaint = new Paint(); + mCandidatesPaint.setAntiAlias(true); + + mFootnotePaint = new Paint(); + mFootnotePaint.setAntiAlias(true); + mFootnotePaint.setColor(r.getColor(R.color.footnote_color)); + mActiveCellRect = new RectF(); + + mCandRects = new Vector<RectF>(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int mOldWidth = mMeasuredWidth; + int mOldHeight = mMeasuredHeight; + + setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), + widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), + heightMeasureSpec)); + + if (mOldWidth != mMeasuredWidth || mOldHeight != mMeasuredHeight) { + onSizeChanged(); + } + } + + public void initialize(ArrowUpdater arrowUpdater, BalloonHint balloonHint, + GestureDetector gestureDetector, CandidateViewListener cvListener) { + mArrowUpdater = arrowUpdater; + mBalloonHint = balloonHint; + mGestureDetector = gestureDetector; + mCvListener = cvListener; + } + + public void setDecodingInfo(DecodingInfo decInfo) { + if (null == decInfo) return; + mDecInfo = decInfo; + mPageNoCalculated = -1; + + if (mDecInfo.candidatesFromApp()) { + mNormalCandidateColor = mRecommendedCandidateColor; + mCandidateTextSize = mRecommendedCandidateTextSize; + } else { + mNormalCandidateColor = mImeCandidateColor; + mCandidateTextSize = mImeCandidateTextSize; + } + if (mCandidatesPaint.getTextSize() != mCandidateTextSize) { + mCandidatesPaint.setTextSize(mCandidateTextSize); + mFmiCandidates = mCandidatesPaint.getFontMetricsInt(); + mSuspensionPointsWidth = + mCandidatesPaint.measureText(SUSPENSION_POINTS); + } + + // Remove any pending timer for the previous list. + mTimer.removeTimer(); + } + + public int getActiveCandiatePosInPage() { + return mActiveCandInPage; + } + + public int getActiveCandiatePosGlobal() { + return mDecInfo.mPageStart.get(mPageNo) + mActiveCandInPage; + } + + /** + * Show a page in the decoding result set previously. + * + * @param pageNo Which page to show. + * @param activeCandInPage Which candidate should be set as active item. + * @param enableActiveHighlight When false, active item will not be + * highlighted. + */ + public void showPage(int pageNo, int activeCandInPage, + boolean enableActiveHighlight) { + if (null == mDecInfo) return; + mPageNo = pageNo; + mActiveCandInPage = activeCandInPage; + if (mEnableActiveHighlight != enableActiveHighlight) { + mEnableActiveHighlight = enableActiveHighlight; + } + + if (!calculatePage(mPageNo)) { + mUpdateArrowStatusWhenDraw = true; + } else { + mUpdateArrowStatusWhenDraw = false; + } + + invalidate(); + } + + public void enableActiveHighlight(boolean enableActiveHighlight) { + if (enableActiveHighlight == mEnableActiveHighlight) return; + + mEnableActiveHighlight = enableActiveHighlight; + invalidate(); + } + + public boolean activeCursorForward() { + if (!mDecInfo.pageReady(mPageNo)) return false; + int pageSize = mDecInfo.mPageStart.get(mPageNo + 1) + - mDecInfo.mPageStart.get(mPageNo); + if (mActiveCandInPage + 1 < pageSize) { + showPage(mPageNo, mActiveCandInPage + 1, true); + return true; + } + return false; + } + + public boolean activeCurseBackward() { + if (mActiveCandInPage > 0) { + showPage(mPageNo, mActiveCandInPage - 1, true); + return true; + } + return false; + } + + private void onSizeChanged() { + mContentWidth = mMeasuredWidth - mPaddingLeft - mPaddingRight; + mContentHeight = (int) ((mMeasuredHeight - mPaddingTop - mPaddingBottom) * 0.95f); + /** + * How to decide the font size if the height for display is given? + * Now it is implemented in a stupid way. + */ + int textSize = 1; + mCandidatesPaint.setTextSize(textSize); + mFmiCandidates = mCandidatesPaint.getFontMetricsInt(); + while (mFmiCandidates.bottom - mFmiCandidates.top < mContentHeight) { + textSize++; + mCandidatesPaint.setTextSize(textSize); + mFmiCandidates = mCandidatesPaint.getFontMetricsInt(); + } + + mImeCandidateTextSize = textSize; + mRecommendedCandidateTextSize = textSize * 3 / 4; + if (null == mDecInfo) { + mCandidateTextSize = mImeCandidateTextSize; + mCandidatesPaint.setTextSize(mCandidateTextSize); + mFmiCandidates = mCandidatesPaint.getFontMetricsInt(); + mSuspensionPointsWidth = + mCandidatesPaint.measureText(SUSPENSION_POINTS); + } else { + // Reset the decoding information to update members for painting. + setDecodingInfo(mDecInfo); + } + + textSize = 1; + mFootnotePaint.setTextSize(textSize); + mFmiFootnote = mFootnotePaint.getFontMetricsInt(); + while (mFmiFootnote.bottom - mFmiFootnote.top < mContentHeight / 2) { + textSize++; + mFootnotePaint.setTextSize(textSize); + mFmiFootnote = mFootnotePaint.getFontMetricsInt(); + } + textSize--; + mFootnotePaint.setTextSize(textSize); + mFmiFootnote = mFootnotePaint.getFontMetricsInt(); + + // When the size is changed, the first page will be displayed. + mPageNo = 0; + mActiveCandInPage = 0; + } + + private boolean calculatePage(int pageNo) { + if (pageNo == mPageNoCalculated) return true; + + mContentWidth = mMeasuredWidth - mPaddingLeft - mPaddingRight; + mContentHeight = (int) ((mMeasuredHeight - mPaddingTop - mPaddingBottom) * 0.95f); + + if (mContentWidth <= 0 || mContentHeight <= 0) return false; + + int candSize = mDecInfo.mCandidatesList.size(); + + // If the size of page exists, only calculate the extra margin. + boolean onlyExtraMargin = false; + int fromPage = mDecInfo.mPageStart.size() - 1; + if (mDecInfo.mPageStart.size() > pageNo + 1) { + onlyExtraMargin = true; + fromPage = pageNo; + } + + // If the previous pages have no information, calculate them first. + for (int p = fromPage; p <= pageNo; p++) { + int pStart = mDecInfo.mPageStart.get(p); + int pSize = 0; + int charNum = 0; + float lastItemWidth = 0; + + float xPos; + xPos = 0; + xPos += mSeparatorDrawable.getIntrinsicWidth(); + while (xPos < mContentWidth && pStart + pSize < candSize) { + int itemPos = pStart + pSize; + String itemStr = mDecInfo.mCandidatesList.get(itemPos); + float itemWidth = mCandidatesPaint.measureText(itemStr); + if (itemWidth < MIN_ITEM_WIDTH) itemWidth = MIN_ITEM_WIDTH; + + itemWidth += mCandidateMargin * 2; + itemWidth += mSeparatorDrawable.getIntrinsicWidth(); + if (xPos + itemWidth < mContentWidth || 0 == pSize) { + xPos += itemWidth; + lastItemWidth = itemWidth; + pSize++; + charNum += itemStr.length(); + } else { + break; + } + } + if (!onlyExtraMargin) { + mDecInfo.mPageStart.add(pStart + pSize); + mDecInfo.mCnToPage.add(mDecInfo.mCnToPage.get(p) + charNum); + } + + float marginExtra = (mContentWidth - xPos) / pSize / 2; + + if (mContentWidth - xPos > lastItemWidth) { + // Must be the last page, because if there are more items, + // the next item's width must be less than lastItemWidth. + // In this case, if the last margin is less than the current + // one, the last margin can be used, so that the + // look-and-feeling will be the same as the previous page. + if (mCandidateMarginExtra <= marginExtra) { + marginExtra = mCandidateMarginExtra; + } + } else if (pSize == 1) { + marginExtra = 0; + } + mCandidateMarginExtra = marginExtra; + } + mPageNoCalculated = pageNo; + return true; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + // The invisible candidate view(the one which is not in foreground) can + // also be called to drawn, but its decoding result and candidate list + // may be empty. + if (null == mDecInfo || mDecInfo.isCandidatesListEmpty()) return; + + // Calculate page. If the paging information is ready, the function will + // return at once. + calculatePage(mPageNo); + + int pStart = mDecInfo.mPageStart.get(mPageNo); + int pSize = mDecInfo.mPageStart.get(mPageNo + 1) - pStart; + float candMargin = mCandidateMargin + mCandidateMarginExtra; + if (mActiveCandInPage > pSize - 1) { + mActiveCandInPage = pSize - 1; + } + + mCandRects.removeAllElements(); + + float xPos = mPaddingLeft; + int yPos = (getMeasuredHeight() - + (mFmiCandidates.bottom - mFmiCandidates.top)) / 2 + - mFmiCandidates.top; + xPos += drawVerticalSeparator(canvas, xPos); + for (int i = 0; i < pSize; i++) { + float footnoteSize = 0; + String footnote = null; + if (mShowFootnote) { + footnote = Integer.toString(i + 1); + footnoteSize = mFootnotePaint.measureText(footnote); + assert (footnoteSize < candMargin); + } + String cand = mDecInfo.mCandidatesList.get(pStart + i); + float candidateWidth = mCandidatesPaint.measureText(cand); + float centerOffset = 0; + if (candidateWidth < MIN_ITEM_WIDTH) { + centerOffset = (MIN_ITEM_WIDTH - candidateWidth) / 2; + candidateWidth = MIN_ITEM_WIDTH; + } + + float itemTotalWidth = candidateWidth + 2 * candMargin; + + if (mActiveCandInPage == i && mEnableActiveHighlight) { + mActiveCellRect.set(xPos, mPaddingTop + 1, xPos + + itemTotalWidth, getHeight() - mPaddingBottom - 1); + mActiveCellDrawable.setBounds((int) mActiveCellRect.left, + (int) mActiveCellRect.top, (int) mActiveCellRect.right, + (int) mActiveCellRect.bottom); + mActiveCellDrawable.draw(canvas); + } + + if (mCandRects.size() < pSize) mCandRects.add(new RectF()); + mCandRects.elementAt(i).set(xPos - 1, yPos + mFmiCandidates.top, + xPos + itemTotalWidth + 1, yPos + mFmiCandidates.bottom); + + // Draw footnote + if (mShowFootnote) { + canvas.drawText(footnote, xPos + (candMargin - footnoteSize) + / 2, yPos, mFootnotePaint); + } + + // Left margin + xPos += candMargin; + if (candidateWidth > mContentWidth - xPos - centerOffset) { + cand = getLimitedCandidateForDrawing(cand, + mContentWidth - xPos - centerOffset); + } + if (mActiveCandInPage == i && mEnableActiveHighlight) { + mCandidatesPaint.setColor(mActiveCandidateColor); + } else { + mCandidatesPaint.setColor(mNormalCandidateColor); + } + canvas.drawText(cand, xPos + centerOffset, yPos, + mCandidatesPaint); + + // Candidate and right margin + xPos += candidateWidth + candMargin; + + // Draw the separator between candidates. + xPos += drawVerticalSeparator(canvas, xPos); + } + + // Update the arrow status of the container. + if (null != mArrowUpdater && mUpdateArrowStatusWhenDraw) { + mArrowUpdater.updateArrowStatus(); + mUpdateArrowStatusWhenDraw = false; + } + } + + private String getLimitedCandidateForDrawing(String rawCandidate, + float widthToDraw) { + int subLen = rawCandidate.length(); + if (subLen <= 1) return rawCandidate; + do { + subLen--; + float width = mCandidatesPaint.measureText(rawCandidate, 0, subLen); + if (width + mSuspensionPointsWidth <= widthToDraw || 1 >= subLen) { + return rawCandidate.substring(0, subLen) + + SUSPENSION_POINTS; + } + } while (true); + } + + private float drawVerticalSeparator(Canvas canvas, float xPos) { + mSeparatorDrawable.setBounds((int) xPos, mPaddingTop, (int) xPos + + mSeparatorDrawable.getIntrinsicWidth(), getMeasuredHeight() + - mPaddingBottom); + mSeparatorDrawable.draw(canvas); + return mSeparatorDrawable.getIntrinsicWidth(); + } + + private int mapToItemInPage(int x, int y) { + // mCandRects.size() == 0 happens when the page is set, but + // touch events occur before onDraw(). It usually happens with + // monkey test. + if (!mDecInfo.pageReady(mPageNo) || mPageNoCalculated != mPageNo + || mCandRects.size() == 0) { + return -1; + } + + int pageStart = mDecInfo.mPageStart.get(mPageNo); + int pageSize = mDecInfo.mPageStart.get(mPageNo + 1) - pageStart; + if (mCandRects.size() < pageSize) { + return -1; + } + + // If not found, try to find the nearest one. + float nearestDis = Float.MAX_VALUE; + int nearest = -1; + for (int i = 0; i < pageSize; i++) { + RectF r = mCandRects.elementAt(i); + if (r.left < x && r.right > x && r.top < y && r.bottom > y) { + return i; + } + float disx = (r.left + r.right) / 2 - x; + float disy = (r.top + r.bottom) / 2 - y; + float dis = disx * disx + disy * disy; + if (dis < nearestDis) { + nearestDis = dis; + nearest = i; + } + } + + return nearest; + } + + // Because the candidate view under the current focused one may also get + // touching events. Here we just bypass the event to the container and let + // it decide which view should handle the event. + @Override + public boolean onTouchEvent(MotionEvent event) { + return super.onTouchEvent(event); + } + + public boolean onTouchEventReal(MotionEvent event) { + // The page in the background can also be touched. + if (null == mDecInfo || !mDecInfo.pageReady(mPageNo) + || mPageNoCalculated != mPageNo) return true; + + int x, y; + x = (int) event.getX(); + y = (int) event.getY(); + + if (mGestureDetector.onTouchEvent(event)) { + mTimer.removeTimer(); + mBalloonHint.delayedDismiss(0); + return true; + } + + int clickedItemInPage = -1; + + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + clickedItemInPage = mapToItemInPage(x, y); + if (clickedItemInPage >= 0) { + invalidate(); + mCvListener.onClickChoice(clickedItemInPage + + mDecInfo.mPageStart.get(mPageNo)); + } + mBalloonHint.delayedDismiss(BalloonHint.TIME_DELAY_DISMISS); + break; + + case MotionEvent.ACTION_DOWN: + clickedItemInPage = mapToItemInPage(x, y); + if (clickedItemInPage >= 0) { + showBalloon(clickedItemInPage, true); + mTimer.startTimer(BalloonHint.TIME_DELAY_SHOW, mPageNo, + clickedItemInPage); + } + break; + + case MotionEvent.ACTION_CANCEL: + break; + + case MotionEvent.ACTION_MOVE: + clickedItemInPage = mapToItemInPage(x, y); + if (clickedItemInPage >= 0 + && (clickedItemInPage != mTimer.getActiveCandOfPageToShow() || mPageNo != mTimer + .getPageToShow())) { + showBalloon(clickedItemInPage, true); + mTimer.startTimer(BalloonHint.TIME_DELAY_SHOW, mPageNo, + clickedItemInPage); + } + } + return true; + } + + private void showBalloon(int candPos, boolean delayedShow) { + mBalloonHint.removeTimer(); + + RectF r = mCandRects.elementAt(candPos); + int desired_width = (int) (r.right - r.left); + int desired_height = (int) (r.bottom - r.top); + mBalloonHint.setBalloonConfig(mDecInfo.mCandidatesList + .get(mDecInfo.mPageStart.get(mPageNo) + candPos), 44, true, + mImeCandidateColor, desired_width, desired_height); + + getLocationOnScreen(mLocationTmp); + mHintPositionToInputView[0] = mLocationTmp[0] + + (int) (r.left - (mBalloonHint.getWidth() - desired_width) / 2); + mHintPositionToInputView[1] = -mBalloonHint.getHeight(); + + long delay = BalloonHint.TIME_DELAY_SHOW; + if (!delayedShow) delay = 0; + mBalloonHint.dismiss(); + if (!mBalloonHint.isShowing()) { + mBalloonHint.delayedShow(delay, mHintPositionToInputView); + } else { + mBalloonHint.delayedUpdate(0, mHintPositionToInputView, -1, -1); + } + } + + private class PressTimer extends Handler implements Runnable { + private boolean mTimerPending = false; + private int mPageNoToShow; + private int mActiveCandOfPage; + + public PressTimer() { + super(); + } + + public void startTimer(long afterMillis, int pageNo, int activeInPage) { + mTimer.removeTimer(); + postDelayed(this, afterMillis); + mTimerPending = true; + mPageNoToShow = pageNo; + mActiveCandOfPage = activeInPage; + } + + public int getPageToShow() { + return mPageNoToShow; + } + + public int getActiveCandOfPageToShow() { + return mActiveCandOfPage; + } + + public boolean removeTimer() { + if (mTimerPending) { + mTimerPending = false; + removeCallbacks(this); + return true; + } + return false; + } + + public boolean isPending() { + return mTimerPending; + } + + public void run() { + if (mPageNoToShow >= 0 && mActiveCandOfPage >= 0) { + // Always enable to highlight the clicked one. + showPage(mPageNoToShow, mActiveCandOfPage, true); + invalidate(); + } + mTimerPending = false; + } + } +} diff --git a/src/com/android/inputmethod/pinyin/CandidateViewListener.java b/src/com/android/inputmethod/pinyin/CandidateViewListener.java new file mode 100644 index 0000000..795d119 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/CandidateViewListener.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2009 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.pinyin; + +/** + * Interface to notify the input method when the user clicks a candidate or + * makes a direction-gesture on candidate view. + */ +public interface CandidateViewListener { + public void onClickChoice(int choiceId); + + public void onToLeftGesture(); + + public void onToRightGesture(); + + public void onToTopGesture(); + + public void onToBottomGesture(); +} diff --git a/src/com/android/inputmethod/pinyin/CandidatesContainer.java b/src/com/android/inputmethod/pinyin/CandidatesContainer.java new file mode 100644 index 0000000..5b2a999 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/CandidatesContainer.java @@ -0,0 +1,474 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import com.android.inputmethod.pinyin.PinyinIME.DecodingInfo; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.TranslateAnimation; +import android.view.animation.Animation.AnimationListener; +import android.widget.ImageButton; +import android.widget.RelativeLayout; +import android.widget.ViewFlipper; + +interface ArrowUpdater { + void updateArrowStatus(); +} + + +/** + * Container used to host the two candidate views. When user drags on candidate + * view, animation is used to dismiss the current candidate view and show a new + * one. These two candidate views and their parent are hosted by this container. + * <p> + * Besides the candidate views, there are two arrow views to show the page + * forward/backward arrows. + * </p> + */ +public class CandidatesContainer extends RelativeLayout implements + OnTouchListener, AnimationListener, ArrowUpdater { + /** + * Alpha value to show an enabled arrow. + */ + private static int ARROW_ALPHA_ENABLED = 0xff; + + /** + * Alpha value to show an disabled arrow. + */ + private static int ARROW_ALPHA_DISABLED = 0x40; + + /** + * Animation time to show a new candidate view and dismiss the old one. + */ + private static int ANIMATION_TIME = 200; + + /** + * Listener used to notify IME that user clicks a candidate, or navigate + * between them. + */ + private CandidateViewListener mCvListener; + + /** + * The left arrow button used to show previous page. + */ + private ImageButton mLeftArrowBtn; + + /** + * The right arrow button used to show next page. + */ + private ImageButton mRightArrowBtn; + + /** + * Decoding result to show. + */ + private DecodingInfo mDecInfo; + + /** + * The animation view used to show candidates. It contains two views. + * Normally, the candidates are shown one of them. When user navigates to + * another page, animation effect will be performed. + */ + private ViewFlipper mFlipper; + + /** + * The x offset of the flipper in this container. + */ + private int xOffsetForFlipper; + + /** + * Animation used by the incoming view when the user navigates to a left + * page. + */ + private Animation mInAnimPushLeft; + + /** + * Animation used by the incoming view when the user navigates to a right + * page. + */ + private Animation mInAnimPushRight; + + /** + * Animation used by the incoming view when the user navigates to a page + * above. If the page navigation is triggered by DOWN key, this animation is + * used. + */ + private Animation mInAnimPushUp; + + /** + * Animation used by the incoming view when the user navigates to a page + * below. If the page navigation is triggered by UP key, this animation is + * used. + */ + private Animation mInAnimPushDown; + + /** + * Animation used by the outgoing view when the user navigates to a left + * page. + */ + private Animation mOutAnimPushLeft; + + /** + * Animation used by the outgoing view when the user navigates to a right + * page. + */ + private Animation mOutAnimPushRight; + + /** + * Animation used by the outgoing view when the user navigates to a page + * above. If the page navigation is triggered by DOWN key, this animation is + * used. + */ + private Animation mOutAnimPushUp; + + /** + * Animation used by the incoming view when the user navigates to a page + * below. If the page navigation is triggered by UP key, this animation is + * used. + */ + private Animation mOutAnimPushDown; + + /** + * Animation object which is used for the incoming view currently. + */ + private Animation mInAnimInUse; + + /** + * Animation object which is used for the outgoing view currently. + */ + private Animation mOutAnimInUse; + + /** + * Current page number in display. + */ + private int mCurrentPage = -1; + + public CandidatesContainer(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void initialize(CandidateViewListener cvListener, + BalloonHint balloonHint, GestureDetector gestureDetector) { + mCvListener = cvListener; + + mLeftArrowBtn = (ImageButton) findViewById(R.id.arrow_left_btn); + mRightArrowBtn = (ImageButton) findViewById(R.id.arrow_right_btn); + mLeftArrowBtn.setOnTouchListener(this); + mRightArrowBtn.setOnTouchListener(this); + + mFlipper = (ViewFlipper) findViewById(R.id.candidate_flipper); + mFlipper.setMeasureAllChildren(true); + + invalidate(); + requestLayout(); + + for (int i = 0; i < mFlipper.getChildCount(); i++) { + CandidateView cv = (CandidateView) mFlipper.getChildAt(i); + cv.initialize(this, balloonHint, gestureDetector, mCvListener); + } + } + + public void showCandidates(PinyinIME.DecodingInfo decInfo, + boolean enableActiveHighlight) { + if (null == decInfo) return; + mDecInfo = decInfo; + mCurrentPage = 0; + + if (decInfo.isCandidatesListEmpty()) { + showArrow(mLeftArrowBtn, false); + showArrow(mRightArrowBtn, false); + } else { + showArrow(mLeftArrowBtn, true); + showArrow(mRightArrowBtn, true); + } + + for (int i = 0; i < mFlipper.getChildCount(); i++) { + CandidateView cv = (CandidateView) mFlipper.getChildAt(i); + cv.setDecodingInfo(mDecInfo); + } + stopAnimation(); + + CandidateView cv = (CandidateView) mFlipper.getCurrentView(); + cv.showPage(mCurrentPage, 0, enableActiveHighlight); + + updateArrowStatus(); + invalidate(); + } + + public int getCurrentPage() { + return mCurrentPage; + } + + public void enableActiveHighlight(boolean enableActiveHighlight) { + CandidateView cv = (CandidateView) mFlipper.getCurrentView(); + cv.enableActiveHighlight(enableActiveHighlight); + invalidate(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + Environment env = Environment.getInstance(); + int measuredWidth = env.getScreenWidth(); + int measuredHeight = getPaddingTop(); + measuredHeight += env.getHeightForCandidates(); + widthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, + MeasureSpec.EXACTLY); + heightMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight, + MeasureSpec.EXACTLY); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (null != mLeftArrowBtn) { + xOffsetForFlipper = mLeftArrowBtn.getMeasuredWidth(); + } + } + + public boolean activeCurseBackward() { + if (mFlipper.isFlipping() || null == mDecInfo) { + return false; + } + + CandidateView cv = (CandidateView) mFlipper.getCurrentView(); + + if (cv.activeCurseBackward()) { + cv.invalidate(); + return true; + } else { + return pageBackward(true, true); + } + } + + public boolean activeCurseForward() { + if (mFlipper.isFlipping() || null == mDecInfo) { + return false; + } + + CandidateView cv = (CandidateView) mFlipper.getCurrentView(); + + if (cv.activeCursorForward()) { + cv.invalidate(); + return true; + } else { + return pageForward(true, true); + } + } + + public boolean pageBackward(boolean animLeftRight, + boolean enableActiveHighlight) { + if (null == mDecInfo) return false; + + if (mFlipper.isFlipping() || 0 == mCurrentPage) return false; + + int child = mFlipper.getDisplayedChild(); + int childNext = (child + 1) % 2; + CandidateView cv = (CandidateView) mFlipper.getChildAt(child); + CandidateView cvNext = (CandidateView) mFlipper.getChildAt(childNext); + + mCurrentPage--; + int activeCandInPage = cv.getActiveCandiatePosInPage(); + if (animLeftRight) + activeCandInPage = mDecInfo.mPageStart.elementAt(mCurrentPage + 1) + - mDecInfo.mPageStart.elementAt(mCurrentPage) - 1; + + cvNext.showPage(mCurrentPage, activeCandInPage, enableActiveHighlight); + loadAnimation(animLeftRight, false); + startAnimation(); + + updateArrowStatus(); + return true; + } + + public boolean pageForward(boolean animLeftRight, + boolean enableActiveHighlight) { + if (null == mDecInfo) return false; + + if (mFlipper.isFlipping() || !mDecInfo.preparePage(mCurrentPage + 1)) { + return false; + } + + int child = mFlipper.getDisplayedChild(); + int childNext = (child + 1) % 2; + CandidateView cv = (CandidateView) mFlipper.getChildAt(child); + int activeCandInPage = cv.getActiveCandiatePosInPage(); + cv.enableActiveHighlight(enableActiveHighlight); + + CandidateView cvNext = (CandidateView) mFlipper.getChildAt(childNext); + mCurrentPage++; + if (animLeftRight) activeCandInPage = 0; + + cvNext.showPage(mCurrentPage, activeCandInPage, enableActiveHighlight); + loadAnimation(animLeftRight, true); + startAnimation(); + + updateArrowStatus(); + return true; + } + + public int getActiveCandiatePos() { + if (null == mDecInfo) return -1; + CandidateView cv = (CandidateView) mFlipper.getCurrentView(); + return cv.getActiveCandiatePosGlobal(); + } + + public void updateArrowStatus() { + if (mCurrentPage < 0) return; + boolean forwardEnabled = mDecInfo.pageForwardable(mCurrentPage); + boolean backwardEnabled = mDecInfo.pageBackwardable(mCurrentPage); + + if (backwardEnabled) { + enableArrow(mLeftArrowBtn, true); + } else { + enableArrow(mLeftArrowBtn, false); + } + if (forwardEnabled) { + enableArrow(mRightArrowBtn, true); + } else { + enableArrow(mRightArrowBtn, false); + } + } + + private void enableArrow(ImageButton arrowBtn, boolean enabled) { + arrowBtn.setEnabled(enabled); + if (enabled) + arrowBtn.setAlpha(ARROW_ALPHA_ENABLED); + else + arrowBtn.setAlpha(ARROW_ALPHA_DISABLED); + } + + private void showArrow(ImageButton arrowBtn, boolean show) { + if (show) + arrowBtn.setVisibility(View.VISIBLE); + else + arrowBtn.setVisibility(View.INVISIBLE); + } + + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (v == mLeftArrowBtn) { + mCvListener.onToRightGesture(); + } else if (v == mRightArrowBtn) { + mCvListener.onToLeftGesture(); + } + } else if (event.getAction() == MotionEvent.ACTION_UP) { + CandidateView cv = (CandidateView) mFlipper.getCurrentView(); + cv.enableActiveHighlight(true); + } + + return false; + } + + // The reason why we handle candiate view's touch events here is because + // that the view under the focused view may get touch events instead of the + // focused one. + @Override + public boolean onTouchEvent(MotionEvent event) { + event.offsetLocation(-xOffsetForFlipper, 0); + CandidateView cv = (CandidateView) mFlipper.getCurrentView(); + cv.onTouchEventReal(event); + return true; + } + + public void loadAnimation(boolean animLeftRight, boolean forward) { + if (animLeftRight) { + if (forward) { + if (null == mInAnimPushLeft) { + mInAnimPushLeft = createAnimation(1.0f, 0, 0, 0, 0, 1.0f, + ANIMATION_TIME); + mOutAnimPushLeft = createAnimation(0, -1.0f, 0, 0, 1.0f, 0, + ANIMATION_TIME); + } + mInAnimInUse = mInAnimPushLeft; + mOutAnimInUse = mOutAnimPushLeft; + } else { + if (null == mInAnimPushRight) { + mInAnimPushRight = createAnimation(-1.0f, 0, 0, 0, 0, 1.0f, + ANIMATION_TIME); + mOutAnimPushRight = createAnimation(0, 1.0f, 0, 0, 1.0f, 0, + ANIMATION_TIME); + } + mInAnimInUse = mInAnimPushRight; + mOutAnimInUse = mOutAnimPushRight; + } + } else { + if (forward) { + if (null == mInAnimPushUp) { + mInAnimPushUp = createAnimation(0, 0, 1.0f, 0, 0, 1.0f, + ANIMATION_TIME); + mOutAnimPushUp = createAnimation(0, 0, 0, -1.0f, 1.0f, 0, + ANIMATION_TIME); + } + mInAnimInUse = mInAnimPushUp; + mOutAnimInUse = mOutAnimPushUp; + } else { + if (null == mInAnimPushDown) { + mInAnimPushDown = createAnimation(0, 0, -1.0f, 0, 0, 1.0f, + ANIMATION_TIME); + mOutAnimPushDown = createAnimation(0, 0, 0, 1.0f, 1.0f, 0, + ANIMATION_TIME); + } + mInAnimInUse = mInAnimPushDown; + mOutAnimInUse = mOutAnimPushDown; + } + } + + mInAnimInUse.setAnimationListener(this); + + mFlipper.setInAnimation(mInAnimInUse); + mFlipper.setOutAnimation(mOutAnimInUse); + } + + private Animation createAnimation(float xFrom, float xTo, float yFrom, + float yTo, float alphaFrom, float alphaTo, long duration) { + AnimationSet animSet = new AnimationSet(getContext(), null); + Animation trans = new TranslateAnimation(Animation.RELATIVE_TO_SELF, + xFrom, Animation.RELATIVE_TO_SELF, xTo, + Animation.RELATIVE_TO_SELF, yFrom, Animation.RELATIVE_TO_SELF, + yTo); + Animation alpha = new AlphaAnimation(alphaFrom, alphaTo); + animSet.addAnimation(trans); + animSet.addAnimation(alpha); + animSet.setDuration(duration); + return animSet; + } + + private void startAnimation() { + mFlipper.showNext(); + } + + private void stopAnimation() { + mFlipper.stopFlipping(); + } + + public void onAnimationEnd(Animation animation) { + if (!mLeftArrowBtn.isPressed() && !mRightArrowBtn.isPressed()) { + CandidateView cv = (CandidateView) mFlipper.getCurrentView(); + cv.enableActiveHighlight(true); + } + } + + public void onAnimationRepeat(Animation animation) { + } + + public void onAnimationStart(Animation animation) { + } +} diff --git a/src/com/android/inputmethod/pinyin/ComposingView.java b/src/com/android/inputmethod/pinyin/ComposingView.java new file mode 100644 index 0000000..f70af45 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/ComposingView.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.FontMetricsInt; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup.LayoutParams; + +/** + * View used to show composing string (The Pinyin string for the unselected + * syllables and the Chinese string for the selected syllables.) + */ +public class ComposingView extends View { + /** + * <p> + * There are three statuses for the composing view. + * </p> + * + * <p> + * {@link #SHOW_PINYIN} is used to show the current Pinyin string without + * highlighted effect. When user inputs Pinyin characters one by one, the + * Pinyin string will be shown in this mode. + * </p> + * <p> + * {@link #SHOW_STRING_LOWERCASE} is used to show the Pinyin string in + * lowercase with highlighted effect. When user presses UP key and there is + * no fixed Chinese characters, composing view will switch from + * {@link #SHOW_PINYIN} to this mode, and in this mode, user can press + * confirm key to input the lower-case string, so that user can input + * English letter in Chinese mode. + * </p> + * <p> + * {@link #EDIT_PINYIN} is used to edit the Pinyin string (shown with + * highlighted effect). When current status is {@link #SHOW_PINYIN} and user + * presses UP key, if there are fixed Characters, the input method will + * switch to {@link #EDIT_PINYIN} thus user can modify some characters in + * the middle of the Pinyin string. If the current status is + * {@link #SHOW_STRING_LOWERCASE} and user presses LEFT and RIGHT key, it + * will also switch to {@link #EDIT_PINYIN}. + * </p> + * <p> + * Whenever user presses down key, the status switches to + * {@link #SHOW_PINYIN}. + * </p> + * <p> + * When composing view's status is {@link #SHOW_PINYIN}, the IME's status is + * {@link PinyinIME.ImeState#STATE_INPUT}, otherwise, the IME's status + * should be {@link PinyinIME.ImeState#STATE_COMPOSING}. + * </p> + */ + public enum ComposingStatus { + SHOW_PINYIN, SHOW_STRING_LOWERCASE, EDIT_PINYIN, + } + + private static final int LEFT_RIGHT_MARGIN = 5; + + /** + * Used to draw composing string. When drawing the active and idle part of + * the spelling(Pinyin) string, the color may be changed. + */ + private Paint mPaint; + + /** + * Drawable used to draw highlight effect. + */ + private Drawable mHlDrawable; + + /** + * Drawable used to draw cursor for editing mode. + */ + private Drawable mCursor; + + /** + * Used to estimate dimensions to show the string . + */ + private FontMetricsInt mFmi; + + private int mStrColor; + private int mStrColorHl; + private int mStrColorIdle; + + private int mFontSize; + + private ComposingStatus mComposingStatus; + + PinyinIME.DecodingInfo mDecInfo; + + public ComposingView(Context context, AttributeSet attrs) { + super(context, attrs); + + Resources r = context.getResources(); + mHlDrawable = r.getDrawable(R.drawable.composing_hl_bg); + mCursor = r.getDrawable(R.drawable.composing_area_cursor); + + mStrColor = r.getColor(R.color.composing_color); + mStrColorHl = r.getColor(R.color.composing_color_hl); + mStrColorIdle = r.getColor(R.color.composing_color_idle); + + mFontSize = r.getDimensionPixelSize(R.dimen.composing_height); + + mPaint = new Paint(); + mPaint.setColor(mStrColor); + mPaint.setAntiAlias(true); + mPaint.setTextSize(mFontSize); + + mFmi = mPaint.getFontMetricsInt(); + } + + public void reset() { + mComposingStatus = ComposingStatus.SHOW_PINYIN; + } + + /** + * Set the composing string to show. If the IME status is + * {@link PinyinIME.ImeState#STATE_INPUT}, the composing view's status will + * be set to {@link ComposingStatus#SHOW_PINYIN}, otherwise the composing + * view will set its status to {@link ComposingStatus#SHOW_STRING_LOWERCASE} + * or {@link ComposingStatus#EDIT_PINYIN} automatically. + */ + public void setDecodingInfo(PinyinIME.DecodingInfo decInfo, + PinyinIME.ImeState imeStatus) { + mDecInfo = decInfo; + + if (PinyinIME.ImeState.STATE_INPUT == imeStatus) { + mComposingStatus = ComposingStatus.SHOW_PINYIN; + mDecInfo.moveCursorToEdge(false); + } else { + if (decInfo.getFixedLen() != 0 + || ComposingStatus.EDIT_PINYIN == mComposingStatus) { + mComposingStatus = ComposingStatus.EDIT_PINYIN; + } else { + mComposingStatus = ComposingStatus.SHOW_STRING_LOWERCASE; + } + mDecInfo.moveCursor(0); + } + + measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + requestLayout(); + invalidate(); + } + + public boolean moveCursor(int keyCode) { + if (keyCode != KeyEvent.KEYCODE_DPAD_LEFT + && keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) return false; + + if (ComposingStatus.EDIT_PINYIN == mComposingStatus) { + int offset = 0; + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) + offset = -1; + else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) offset = 1; + mDecInfo.moveCursor(offset); + } else if (ComposingStatus.SHOW_STRING_LOWERCASE == mComposingStatus) { + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + mComposingStatus = ComposingStatus.EDIT_PINYIN; + + measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + requestLayout(); + } + + } + invalidate(); + return true; + } + + public ComposingStatus getComposingStatus() { + return mComposingStatus; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + float width; + int height; + height = mFmi.bottom - mFmi.top + mPaddingTop + mPaddingBottom; + + if (null == mDecInfo) { + width = 0; + } else { + width = mPaddingLeft + mPaddingRight + LEFT_RIGHT_MARGIN * 2; + + String str; + if (ComposingStatus.SHOW_STRING_LOWERCASE == mComposingStatus) { + str = mDecInfo.getOrigianlSplStr().toString(); + } else { + str = mDecInfo.getComposingStrForDisplay(); + } + width += mPaint.measureText(str, 0, str.length()); + } + setMeasuredDimension((int) (width + 0.5f), height); + } + + @Override + protected void onDraw(Canvas canvas) { + if (ComposingStatus.EDIT_PINYIN == mComposingStatus + || ComposingStatus.SHOW_PINYIN == mComposingStatus) { + drawForPinyin(canvas); + return; + } + + float x, y; + x = mPaddingLeft + LEFT_RIGHT_MARGIN; + y = -mFmi.top + mPaddingTop; + + mPaint.setColor(mStrColorHl); + mHlDrawable.setBounds(mPaddingLeft, mPaddingTop, getWidth() + - mPaddingRight, getHeight() - mPaddingBottom); + mHlDrawable.draw(canvas); + + String splStr = mDecInfo.getOrigianlSplStr().toString(); + canvas.drawText(splStr, 0, splStr.length(), x, y, mPaint); + } + + private void drawCursor(Canvas canvas, float x) { + mCursor.setBounds((int) x, mPaddingTop, (int) x + + mCursor.getIntrinsicWidth(), getHeight() - mPaddingBottom); + mCursor.draw(canvas); + } + + private void drawForPinyin(Canvas canvas) { + float x, y; + x = mPaddingLeft + LEFT_RIGHT_MARGIN; + y = -mFmi.top + mPaddingTop; + + mPaint.setColor(mStrColor); + + int cursorPos = mDecInfo.getCursorPosInCmpsDisplay(); + int cmpsPos = cursorPos; + String cmpsStr = mDecInfo.getComposingStrForDisplay(); + int activeCmpsLen = mDecInfo.getActiveCmpsDisplayLen(); + if (cursorPos > activeCmpsLen) cmpsPos = activeCmpsLen; + canvas.drawText(cmpsStr, 0, cmpsPos, x, y, mPaint); + x += mPaint.measureText(cmpsStr, 0, cmpsPos); + if (cursorPos <= activeCmpsLen) { + if (ComposingStatus.EDIT_PINYIN == mComposingStatus) { + drawCursor(canvas, x); + } + canvas.drawText(cmpsStr, cmpsPos, activeCmpsLen, x, y, mPaint); + } + + x += mPaint.measureText(cmpsStr, cmpsPos, activeCmpsLen); + + if (cmpsStr.length() > activeCmpsLen) { + mPaint.setColor(mStrColorIdle); + int oriPos = activeCmpsLen; + if (cursorPos > activeCmpsLen) { + if (cursorPos > cmpsStr.length()) cursorPos = cmpsStr.length(); + canvas.drawText(cmpsStr, oriPos, cursorPos, x, y, mPaint); + x += mPaint.measureText(cmpsStr, oriPos, cursorPos); + + if (ComposingStatus.EDIT_PINYIN == mComposingStatus) { + drawCursor(canvas, x); + } + + oriPos = cursorPos; + } + canvas.drawText(cmpsStr, oriPos, cmpsStr.length(), x, y, mPaint); + } + } +} diff --git a/src/com/android/inputmethod/pinyin/EnglishInputProcessor.java b/src/com/android/inputmethod/pinyin/EnglishInputProcessor.java new file mode 100644 index 0000000..6d61119 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/EnglishInputProcessor.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import android.view.KeyEvent; +import android.view.inputmethod.InputConnection; + +/** + * Class to handle English input. + */ +public class EnglishInputProcessor { + + private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; + + public boolean processKey(InputConnection inputContext, KeyEvent event, + boolean upperCase, boolean realAction) { + if (null == inputContext || null == event) return false; + + int keyCode = event.getKeyCode(); + + CharSequence prefix = null; + prefix = inputContext.getTextBeforeCursor(2, 0); + + int keyChar; + keyChar = 0; + if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) { + keyChar = keyCode - KeyEvent.KEYCODE_A + 'a'; + if (upperCase) { + keyChar = keyChar + 'A' - 'a'; + } + } else if (keyCode >= KeyEvent.KEYCODE_0 + && keyCode <= KeyEvent.KEYCODE_9) + keyChar = keyCode - KeyEvent.KEYCODE_0 + '0'; + else if (keyCode == KeyEvent.KEYCODE_COMMA) + keyChar = ','; + else if (keyCode == KeyEvent.KEYCODE_PERIOD) + keyChar = '.'; + else if (keyCode == KeyEvent.KEYCODE_APOSTROPHE) + keyChar = '\''; + else if (keyCode == KeyEvent.KEYCODE_AT) + keyChar = '@'; + else if (keyCode == KeyEvent.KEYCODE_SLASH) keyChar = '/'; + + if (0 == keyChar) { + mLastKeyCode = keyCode; + + String insert = null; + if (KeyEvent.KEYCODE_DEL == keyCode) { + if (realAction) { + inputContext.deleteSurroundingText(1, 0); + } + } else if (KeyEvent.KEYCODE_ENTER == keyCode) { + insert = "\n"; + } else if (KeyEvent.KEYCODE_SPACE == keyCode) { + insert = " "; + } else { + return false; + } + + if (null != insert && realAction) + inputContext.commitText(insert, insert.length()); + + return true; + } + + if (!realAction) + return true; + + if (KeyEvent.KEYCODE_SHIFT_LEFT == mLastKeyCode + || KeyEvent.KEYCODE_SHIFT_LEFT == mLastKeyCode) { + if (keyChar >= 'a' && keyChar <= 'z') + keyChar = keyChar - 'a' + 'A'; + } else if (KeyEvent.KEYCODE_ALT_LEFT == mLastKeyCode) { + } + + String result = String.valueOf((char) keyChar); + inputContext.commitText(result, result.length()); + mLastKeyCode = keyCode; + return true; + } +} diff --git a/src/com/android/inputmethod/pinyin/Environment.java b/src/com/android/inputmethod/pinyin/Environment.java new file mode 100644 index 0000000..8869294 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/Environment.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import android.content.Context; +import android.content.res.Configuration; +import android.view.Display; +import android.view.WindowManager; + +/** + * Global environment configurations for showing soft keyboard and candidate + * view. All original dimension values are defined in float, and the real size + * is calculated from the float values of and screen size. In this way, this + * input method can work even when screen size is changed. + */ +public class Environment { + /** + * The key height for portrait mode. It is relative to the screen height. + */ + private static final float KEY_HEIGHT_RATIO_PORTRAIT = 0.105f; + + /** + * The key height for landscape mode. It is relative to the screen height. + */ + private static final float KEY_HEIGHT_RATIO_LANDSCAPE = 0.147f; + + /** + * The height of the candidates area for portrait mode. It is relative to + * screen height. + */ + private static final float CANDIDATES_AREA_HEIGHT_RATIO_PORTRAIT = 0.084f; + + /** + * The height of the candidates area for portrait mode. It is relative to + * screen height. + */ + private static final float CANDIDATES_AREA_HEIGHT_RATIO_LANDSCAPE = 0.125f; + + /** + * How much should the balloon width be larger than width of the real key. + * It is relative to the smaller one of screen width and height. + */ + private static final float KEY_BALLOON_WIDTH_PLUS_RATIO = 0.08f; + + /** + * How much should the balloon height be larger than that of the real key. + * It is relative to the smaller one of screen width and height. + */ + private static final float KEY_BALLOON_HEIGHT_PLUS_RATIO = 0.07f; + + /** + * The text size for normal keys. It is relative to the smaller one of + * screen width and height. + */ + private static final float NORMAL_KEY_TEXT_SIZE_RATIO = 0.075f; + + /** + * The text size for function keys. It is relative to the smaller one of + * screen width and height. + */ + private static final float FUNCTION_KEY_TEXT_SIZE_RATIO = 0.055f; + + /** + * The text size balloons of normal keys. It is relative to the smaller one + * of screen width and height. + */ + private static final float NORMAL_BALLOON_TEXT_SIZE_RATIO = 0.14f; + + /** + * The text size balloons of function keys. It is relative to the smaller + * one of screen width and height. + */ + private static final float FUNCTION_BALLOON_TEXT_SIZE_RATIO = 0.085f; + + /** + * The configurations are managed in a singleton. + */ + private static Environment mInstance; + + private int mScreenWidth; + private int mScreenHeight; + private int mKeyHeight; + private int mCandidatesAreaHeight; + private int mKeyBalloonWidthPlus; + private int mKeyBalloonHeightPlus; + private int mNormalKeyTextSize; + private int mFunctionKeyTextSize; + private int mNormalBalloonTextSize; + private int mFunctionBalloonTextSize; + private Configuration mConfig = new Configuration(); + private boolean mDebug = false; + + private Environment() { + } + + public static Environment getInstance() { + if (null == mInstance) { + mInstance = new Environment(); + } + return mInstance; + } + + public void onConfigurationChanged(Configuration newConfig, Context context) { + if (mConfig.orientation != newConfig.orientation) { + WindowManager wm = (WindowManager) context + .getSystemService(Context.WINDOW_SERVICE); + Display d = wm.getDefaultDisplay(); + mScreenWidth = d.getWidth(); + mScreenHeight = d.getHeight(); + + int scale; + if (mScreenHeight > mScreenWidth) { + mKeyHeight = (int) (mScreenHeight * KEY_HEIGHT_RATIO_PORTRAIT); + mCandidatesAreaHeight = (int) (mScreenHeight * CANDIDATES_AREA_HEIGHT_RATIO_PORTRAIT); + scale = mScreenWidth; + } else { + mKeyHeight = (int) (mScreenHeight * KEY_HEIGHT_RATIO_LANDSCAPE); + mCandidatesAreaHeight = (int) (mScreenHeight * CANDIDATES_AREA_HEIGHT_RATIO_LANDSCAPE); + scale = mScreenHeight; + } + mNormalKeyTextSize = (int) (scale * NORMAL_KEY_TEXT_SIZE_RATIO); + mFunctionKeyTextSize = (int) (scale * FUNCTION_KEY_TEXT_SIZE_RATIO); + mNormalBalloonTextSize = (int) (scale * NORMAL_BALLOON_TEXT_SIZE_RATIO); + mFunctionBalloonTextSize = (int) (scale * FUNCTION_BALLOON_TEXT_SIZE_RATIO); + mKeyBalloonWidthPlus = (int) (scale * KEY_BALLOON_WIDTH_PLUS_RATIO); + mKeyBalloonHeightPlus = (int) (scale * KEY_BALLOON_HEIGHT_PLUS_RATIO); + } + + mConfig.updateFrom(newConfig); + } + + public Configuration getConfiguration() { + return mConfig; + } + + public int getScreenWidth() { + return mScreenWidth; + } + + public int getScreenHeight() { + return mScreenHeight; + } + + public int getHeightForCandidates() { + return mCandidatesAreaHeight; + } + + public float getKeyXMarginFactor() { + return 1.0f; + } + + public float getKeyYMarginFactor() { + if (Configuration.ORIENTATION_LANDSCAPE == mConfig.orientation) { + return 0.7f; + } + return 1.0f; + } + + public int getKeyHeight() { + return mKeyHeight; + } + + public int getKeyBalloonWidthPlus() { + return mKeyBalloonWidthPlus; + } + + public int getKeyBalloonHeightPlus() { + return mKeyBalloonHeightPlus; + } + + public int getSkbHeight() { + if (Configuration.ORIENTATION_PORTRAIT == mConfig.orientation) { + return mKeyHeight * 4; + } else if (Configuration.ORIENTATION_LANDSCAPE == mConfig.orientation) { + return mKeyHeight * 4; + } + return 0; + } + + public int getKeyTextSize(boolean isFunctionKey) { + if (isFunctionKey) { + return mFunctionKeyTextSize; + } else { + return mNormalKeyTextSize; + } + } + + public int getBalloonTextSize(boolean isFunctionKey) { + if (isFunctionKey) { + return mFunctionBalloonTextSize; + } else { + return mNormalBalloonTextSize; + } + } + + public boolean hasHardKeyboard() { + if (mConfig.keyboard == Configuration.KEYBOARD_NOKEYS + || mConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) { + return false; + } + return true; + } + + public boolean needDebug() { + return mDebug; + } +} diff --git a/src/com/android/inputmethod/pinyin/InputModeSwitcher.java b/src/com/android/inputmethod/pinyin/InputModeSwitcher.java new file mode 100644 index 0000000..7167182 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/InputModeSwitcher.java @@ -0,0 +1,825 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import com.android.inputmethod.pinyin.SoftKeyboard.KeyRow; + +import android.content.res.Resources; +import android.view.inputmethod.EditorInfo; + +/** + * Switcher used to switching input mode between Chinese, English, symbol,etc. + */ +public class InputModeSwitcher { + /** + * User defined key code, used by soft keyboard. + */ + private static final int USERDEF_KEYCODE_SHIFT_1 = -1; + + /** + * User defined key code, used by soft keyboard. + */ + private static final int USERDEF_KEYCODE_LANG_2 = -2; + + /** + * User defined key code, used by soft keyboard. + */ + private static final int USERDEF_KEYCODE_SYM_3 = -3; + + /** + * User defined key code, used by soft keyboard. + */ + public static final int USERDEF_KEYCODE_PHONE_SYM_4 = -4; + + /** + * User defined key code, used by soft keyboard. + */ + private static final int USERDEF_KEYCODE_MORE_SYM_5 = -5; + + /** + * User defined key code, used by soft keyboard. + */ + private static final int USERDEF_KEYCODE_SMILEY_6 = -6; + + + /** + * Bits used to indicate soft keyboard layout. If none bit is set, the + * current input mode does not require a soft keyboard. + **/ + private static final int MASK_SKB_LAYOUT = 0xf0000000; + + /** + * A kind of soft keyboard layout. An input mode should be anded with + * {@link #MASK_SKB_LAYOUT} to get its soft keyboard layout. + */ + private static final int MASK_SKB_LAYOUT_QWERTY = 0x10000000; + + /** + * A kind of soft keyboard layout. An input mode should be anded with + * {@link #MASK_SKB_LAYOUT} to get its soft keyboard layout. + */ + private static final int MASK_SKB_LAYOUT_SYMBOL1 = 0x20000000; + + /** + * A kind of soft keyboard layout. An input mode should be anded with + * {@link #MASK_SKB_LAYOUT} to get its soft keyboard layout. + */ + private static final int MASK_SKB_LAYOUT_SYMBOL2 = 0x30000000; + + /** + * A kind of soft keyboard layout. An input mode should be anded with + * {@link #MASK_SKB_LAYOUT} to get its soft keyboard layout. + */ + private static final int MASK_SKB_LAYOUT_SMILEY = 0x40000000; + + /** + * A kind of soft keyboard layout. An input mode should be anded with + * {@link #MASK_SKB_LAYOUT} to get its soft keyboard layout. + */ + private static final int MASK_SKB_LAYOUT_PHONE = 0x50000000; + + /** + * Used to indicate which language the current input mode is in. If the + * current input mode works with a none-QWERTY soft keyboard, these bits are + * also used to get language information. For example, a Chinese symbol soft + * keyboard and an English one are different in an icon which is used to + * tell user the language information. BTW, the smiley soft keyboard mode + * should be set with {@link #MASK_LANGUAGE_CN} because it can only be + * launched from Chinese QWERTY soft keyboard, and it has Chinese icon on + * soft keyboard. + */ + private static final int MASK_LANGUAGE = 0x0f000000; + + /** + * Used to indicate the current language. An input mode should be anded with + * {@link #MASK_LANGUAGE} to get this information. + */ + private static final int MASK_LANGUAGE_CN = 0x01000000; + + /** + * Used to indicate the current language. An input mode should be anded with + * {@link #MASK_LANGUAGE} to get this information. + */ + private static final int MASK_LANGUAGE_EN = 0x02000000; + + /** + * Used to indicate which case the current input mode is in. For example, + * English QWERTY has lowercase and uppercase. For the Chinese QWERTY, these + * bits are ignored. For phone keyboard layout, these bits can be + * {@link #MASK_CASE_UPPER} to request symbol page for phone soft keyboard. + */ + private static final int MASK_CASE = 0x00f00000; + + /** + * Used to indicate the current case information. An input mode should be + * anded with {@link #MASK_CASE} to get this information. + */ + private static final int MASK_CASE_LOWER = 0x00100000; + + /** + * Used to indicate the current case information. An input mode should be + * anded with {@link #MASK_CASE} to get this information. + */ + private static final int MASK_CASE_UPPER = 0x00200000; + + /** + * Mode for inputing Chinese with soft keyboard. + */ + public static final int MODE_SKB_CHINESE = (MASK_SKB_LAYOUT_QWERTY | MASK_LANGUAGE_CN); + + /** + * Mode for inputing basic symbols for Chinese mode with soft keyboard. + */ + public static final int MODE_SKB_SYMBOL1_CN = (MASK_SKB_LAYOUT_SYMBOL1 | MASK_LANGUAGE_CN); + + /** + * Mode for inputing more symbols for Chinese mode with soft keyboard. + */ + public static final int MODE_SKB_SYMBOL2_CN = (MASK_SKB_LAYOUT_SYMBOL2 | MASK_LANGUAGE_CN); + + /** + * Mode for inputing English lower characters with soft keyboard. + */ + public static final int MODE_SKB_ENGLISH_LOWER = (MASK_SKB_LAYOUT_QWERTY + | MASK_LANGUAGE_EN | MASK_CASE_LOWER); + + /** + * Mode for inputing English upper characters with soft keyboard. + */ + public static final int MODE_SKB_ENGLISH_UPPER = (MASK_SKB_LAYOUT_QWERTY + | MASK_LANGUAGE_EN | MASK_CASE_UPPER); + + /** + * Mode for inputing basic symbols for English mode with soft keyboard. + */ + public static final int MODE_SKB_SYMBOL1_EN = (MASK_SKB_LAYOUT_SYMBOL1 | MASK_LANGUAGE_EN); + + /** + * Mode for inputing more symbols for English mode with soft keyboard. + */ + public static final int MODE_SKB_SYMBOL2_EN = (MASK_SKB_LAYOUT_SYMBOL2 | MASK_LANGUAGE_EN); + + /** + * Mode for inputing smileys with soft keyboard. + */ + public static final int MODE_SKB_SMILEY = (MASK_SKB_LAYOUT_SMILEY | MASK_LANGUAGE_CN); + + /** + * Mode for inputing phone numbers. + */ + public static final int MODE_SKB_PHONE_NUM = (MASK_SKB_LAYOUT_PHONE); + + /** + * Mode for inputing phone numbers. + */ + public static final int MODE_SKB_PHONE_SYM = (MASK_SKB_LAYOUT_PHONE | MASK_CASE_UPPER); + + /** + * Mode for inputing Chinese with a hardware keyboard. + */ + public static final int MODE_HKB_CHINESE = (MASK_LANGUAGE_CN); + + /** + * Mode for inputing English with a hardware keyboard + */ + public static final int MODE_HKB_ENGLISH = (MASK_LANGUAGE_EN); + + /** + * Unset mode. + */ + public static final int MODE_UNSET = 0; + + /** + * Maximum toggle states for a soft keyboard. + */ + public static final int MAX_TOGGLE_STATES = 4; + + /** + * The input mode for the current edit box. + */ + private int mInputMode = MODE_UNSET; + + /** + * Used to remember previous input mode. When user enters an edit field, the + * previous input mode will be tried. If the previous mode can not be used + * for the current situation (For example, previous mode is a soft keyboard + * mode to input symbols, and we have a hardware keyboard for the current + * situation), {@link #mRecentLauageInputMode} will be tried. + **/ + private int mPreviousInputMode = MODE_SKB_CHINESE; + + /** + * Used to remember recent mode to input language. + */ + private int mRecentLauageInputMode = MODE_SKB_CHINESE; + + /** + * Editor information of the current edit box. + */ + private EditorInfo mEditorInfo; + + /** + * Used to indicate required toggling operations. + */ + private ToggleStates mToggleStates = new ToggleStates(); + + /** + * The current field is a short message field? + */ + private boolean mShortMessageField; + + /** + * Is return key in normal state? + */ + private boolean mEnterKeyNormal = true; + + /** + * Current icon. 0 for none icon. + */ + int mInputIcon = R.drawable.ime_pinyin; + + /** + * IME service. + */ + private PinyinIME mImeService; + + /** + * Key toggling state for Chinese mode. + */ + private int mToggleStateCn; + + /** + * Key toggling state for Chinese mode with candidates. + */ + private int mToggleStateCnCand; + + /** + * Key toggling state for English lowwercase mode. + */ + private int mToggleStateEnLower; + + /** + * Key toggling state for English upppercase mode. + */ + private int mToggleStateEnUpper; + + /** + * Key toggling state for English symbol mode for the first page. + */ + private int mToggleStateEnSym1; + + /** + * Key toggling state for English symbol mode for the second page. + */ + private int mToggleStateEnSym2; + + /** + * Key toggling state for smiley mode. + */ + private int mToggleStateSmiley; + + /** + * Key toggling state for phone symbol mode. + */ + private int mToggleStatePhoneSym; + + /** + * Key toggling state for GO action of ENTER key. + */ + private int mToggleStateGo; + + /** + * Key toggling state for SEARCH action of ENTER key. + */ + private int mToggleStateSearch; + + /** + * Key toggling state for SEND action of ENTER key. + */ + private int mToggleStateSend; + + /** + * Key toggling state for NEXT action of ENTER key. + */ + private int mToggleStateNext; + + /** + * Key toggling state for SEND action of ENTER key. + */ + private int mToggleStateDone; + + /** + * QWERTY row toggling state for Chinese input. + */ + private int mToggleRowCn; + + /** + * QWERTY row toggling state for English input. + */ + private int mToggleRowEn; + + /** + * QWERTY row toggling state for URI input. + */ + private int mToggleRowUri; + + /** + * QWERTY row toggling state for email address input. + */ + private int mToggleRowEmailAddress; + + class ToggleStates { + /** + * If it is true, this soft keyboard is a QWERTY one. + */ + boolean mQwerty; + + /** + * If {@link #mQwerty} is true, this variable is used to decide the + * letter case of the QWERTY keyboard. + */ + boolean mQwertyUpperCase; + + /** + * The id of enabled row in the soft keyboard. Refer to + * {@link com.android.inputmethod.pinyin.SoftKeyboard.KeyRow} for + * details. + */ + public int mRowIdToEnable; + + /** + * Used to store all other toggle states for the current input mode. + */ + public int mKeyStates[] = new int[MAX_TOGGLE_STATES]; + + /** + * Number of states to toggle. + */ + public int mKeyStatesNum; + } + + public InputModeSwitcher(PinyinIME imeService) { + mImeService = imeService; + Resources r = mImeService.getResources(); + mToggleStateCn = Integer.parseInt(r.getString(R.string.toggle_cn)); + mToggleStateCnCand = Integer.parseInt(r + .getString(R.string.toggle_cn_cand)); + mToggleStateEnLower = Integer.parseInt(r + .getString(R.string.toggle_en_lower)); + mToggleStateEnUpper = Integer.parseInt(r + .getString(R.string.toggle_en_upper)); + mToggleStateEnSym1 = Integer.parseInt(r + .getString(R.string.toggle_en_sym1)); + mToggleStateEnSym2 = Integer.parseInt(r + .getString(R.string.toggle_en_sym2)); + mToggleStateSmiley = Integer.parseInt(r + .getString(R.string.toggle_smiley)); + mToggleStatePhoneSym = Integer.parseInt(r + .getString(R.string.toggle_phone_sym)); + + mToggleStateGo = Integer + .parseInt(r.getString(R.string.toggle_enter_go)); + mToggleStateSearch = Integer.parseInt(r + .getString(R.string.toggle_enter_search)); + mToggleStateSend = Integer.parseInt(r + .getString(R.string.toggle_enter_send)); + mToggleStateNext = Integer.parseInt(r + .getString(R.string.toggle_enter_next)); + mToggleStateDone = Integer.parseInt(r + .getString(R.string.toggle_enter_done)); + + mToggleRowCn = Integer.parseInt(r.getString(R.string.toggle_row_cn)); + mToggleRowEn = Integer.parseInt(r.getString(R.string.toggle_row_en)); + mToggleRowUri = Integer.parseInt(r.getString(R.string.toggle_row_uri)); + mToggleRowEmailAddress = Integer.parseInt(r + .getString(R.string.toggle_row_emailaddress)); + } + + public int getInputMode() { + return mInputMode; + } + + public ToggleStates getToggleStates() { + return mToggleStates; + } + + public int getSkbLayout() { + int layout = (mInputMode & MASK_SKB_LAYOUT); + + switch (layout) { + case MASK_SKB_LAYOUT_QWERTY: + return R.xml.skb_qwerty; + case MASK_SKB_LAYOUT_SYMBOL1: + return R.xml.skb_sym1; + case MASK_SKB_LAYOUT_SYMBOL2: + return R.xml.skb_sym2; + case MASK_SKB_LAYOUT_SMILEY: + return R.xml.skb_smiley; + case MASK_SKB_LAYOUT_PHONE: + return R.xml.skb_phone; + } + return 0; + } + + // Return the icon to update. + public int switchLanguageWithHkb() { + int newInputMode = MODE_HKB_CHINESE; + mInputIcon = R.drawable.ime_pinyin; + + if (MODE_HKB_CHINESE == mInputMode) { + newInputMode = MODE_HKB_ENGLISH; + mInputIcon = R.drawable.ime_en; + } + + saveInputMode(newInputMode); + return mInputIcon; + } + + // Return the icon to update. + public int switchModeForUserKey(int userKey) { + int newInputMode = MODE_UNSET; + + if (USERDEF_KEYCODE_LANG_2 == userKey) { + if (MODE_SKB_CHINESE == mInputMode) { + newInputMode = MODE_SKB_ENGLISH_LOWER; + } else if (MODE_SKB_ENGLISH_LOWER == mInputMode + || MODE_SKB_ENGLISH_UPPER == mInputMode) { + newInputMode = MODE_SKB_CHINESE; + } else if (MODE_SKB_SYMBOL1_CN == mInputMode) { + newInputMode = MODE_SKB_SYMBOL1_EN; + } else if (MODE_SKB_SYMBOL1_EN == mInputMode) { + newInputMode = MODE_SKB_SYMBOL1_CN; + } else if (MODE_SKB_SYMBOL2_CN == mInputMode) { + newInputMode = MODE_SKB_SYMBOL2_EN; + } else if (MODE_SKB_SYMBOL2_EN == mInputMode) { + newInputMode = MODE_SKB_SYMBOL2_CN; + } else if (MODE_SKB_SMILEY == mInputMode) { + newInputMode = MODE_SKB_CHINESE; + } + } else if (USERDEF_KEYCODE_SYM_3 == userKey) { + if (MODE_SKB_CHINESE == mInputMode) { + newInputMode = MODE_SKB_SYMBOL1_CN; + } else if (MODE_SKB_ENGLISH_UPPER == mInputMode + || MODE_SKB_ENGLISH_LOWER == mInputMode) { + newInputMode = MODE_SKB_SYMBOL1_EN; + } else if (MODE_SKB_SYMBOL1_EN == mInputMode + || MODE_SKB_SYMBOL2_EN == mInputMode) { + newInputMode = MODE_SKB_ENGLISH_LOWER; + } else if (MODE_SKB_SYMBOL1_CN == mInputMode + || MODE_SKB_SYMBOL2_CN == mInputMode) { + newInputMode = MODE_SKB_CHINESE; + } else if (MODE_SKB_SMILEY == mInputMode) { + newInputMode = MODE_SKB_SYMBOL1_CN; + } + } else if (USERDEF_KEYCODE_SHIFT_1 == userKey) { + if (MODE_SKB_ENGLISH_LOWER == mInputMode) { + newInputMode = MODE_SKB_ENGLISH_UPPER; + } else if (MODE_SKB_ENGLISH_UPPER == mInputMode) { + newInputMode = MODE_SKB_ENGLISH_LOWER; + } + } else if (USERDEF_KEYCODE_MORE_SYM_5 == userKey) { + int sym = (MASK_SKB_LAYOUT & mInputMode); + if (MASK_SKB_LAYOUT_SYMBOL1 == sym) { + sym = MASK_SKB_LAYOUT_SYMBOL2; + } else { + sym = MASK_SKB_LAYOUT_SYMBOL1; + } + newInputMode = ((mInputMode & (~MASK_SKB_LAYOUT)) | sym); + } else if (USERDEF_KEYCODE_SMILEY_6 == userKey) { + if (MODE_SKB_CHINESE == mInputMode) { + newInputMode = MODE_SKB_SMILEY; + } else { + newInputMode = MODE_SKB_CHINESE; + } + } else if (USERDEF_KEYCODE_PHONE_SYM_4 == userKey) { + if (MODE_SKB_PHONE_NUM == mInputMode) { + newInputMode = MODE_SKB_PHONE_SYM; + } else { + newInputMode = MODE_SKB_PHONE_NUM; + } + } + + if (newInputMode == mInputMode || MODE_UNSET == newInputMode) { + return mInputIcon; + } + + saveInputMode(newInputMode); + prepareToggleStates(true); + return mInputIcon; + } + + // Return the icon to update. + public int requestInputWithHkb(EditorInfo editorInfo) { + mShortMessageField = false; + boolean english = false; + int newInputMode = MODE_HKB_CHINESE; + + switch (editorInfo.inputType & EditorInfo.TYPE_MASK_CLASS) { + case EditorInfo.TYPE_CLASS_NUMBER: + case EditorInfo.TYPE_CLASS_PHONE: + case EditorInfo.TYPE_CLASS_DATETIME: + english = true; + break; + case EditorInfo.TYPE_CLASS_TEXT: + int v = editorInfo.inputType & EditorInfo.TYPE_MASK_VARIATION; + if (v == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + || v == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD + || v == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + || v == EditorInfo.TYPE_TEXT_VARIATION_URI) { + english = true; + } else if (v == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { + mShortMessageField = true; + } + break; + default: + } + + if (english) { + // If the application request English mode, we switch to it. + newInputMode = MODE_HKB_ENGLISH; + } else { + // If the application do not request English mode, we will + // try to keep the previous mode to input language text. + // Because there is not soft keyboard, we need discard all + // soft keyboard related information from the previous language + // mode. + if ((mRecentLauageInputMode & MASK_LANGUAGE) == MASK_LANGUAGE_CN) { + newInputMode = MODE_HKB_CHINESE; + } else { + newInputMode = MODE_HKB_ENGLISH; + } + } + mEditorInfo = editorInfo; + saveInputMode(newInputMode); + prepareToggleStates(false); + return mInputIcon; + } + + // Return the icon to update. + public int requestInputWithSkb(EditorInfo editorInfo) { + mShortMessageField = false; + + int newInputMode = MODE_SKB_CHINESE; + + switch (editorInfo.inputType & EditorInfo.TYPE_MASK_CLASS) { + case EditorInfo.TYPE_CLASS_NUMBER: + case EditorInfo.TYPE_CLASS_DATETIME: + newInputMode = MODE_SKB_SYMBOL1_EN; + break; + case EditorInfo.TYPE_CLASS_PHONE: + newInputMode = MODE_SKB_PHONE_NUM; + break; + case EditorInfo.TYPE_CLASS_TEXT: + int v = editorInfo.inputType & EditorInfo.TYPE_MASK_VARIATION; + if (v == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + || v == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD + || v == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + || v == EditorInfo.TYPE_TEXT_VARIATION_URI) { + // If the application request English mode, we switch to it. + newInputMode = MODE_SKB_ENGLISH_LOWER; + } else { + if (v == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { + mShortMessageField = true; + } + // If the application do not request English mode, we will + // try to keep the previous mode. + int skbLayout = (mInputMode & MASK_SKB_LAYOUT); + newInputMode = mInputMode; + if (0 == skbLayout) { + if ((mInputMode & MASK_LANGUAGE) == MASK_LANGUAGE_CN) { + newInputMode = MODE_SKB_CHINESE; + } else { + newInputMode = MODE_SKB_ENGLISH_LOWER; + } + } + } + break; + default: + // Try to keep the previous mode. + int skbLayout = (mInputMode & MASK_SKB_LAYOUT); + newInputMode = mInputMode; + if (0 == skbLayout) { + if ((mInputMode & MASK_LANGUAGE) == MASK_LANGUAGE_CN) { + newInputMode = MODE_SKB_CHINESE; + } else { + newInputMode = MODE_SKB_ENGLISH_LOWER; + } + } + break; + } + + mEditorInfo = editorInfo; + saveInputMode(newInputMode); + prepareToggleStates(true); + return mInputIcon; + } + + // Return the icon to update. + public int requestBackToPreviousSkb() { + int layout = (mInputMode & MASK_SKB_LAYOUT); + int lastLayout = (mPreviousInputMode & MASK_SKB_LAYOUT); + if (0 != layout && 0 != lastLayout) { + mInputMode = mPreviousInputMode; + saveInputMode(mInputMode); + prepareToggleStates(true); + return mInputIcon; + } + return 0; + } + + public int getTooggleStateForCnCand() { + return mToggleStateCnCand; + } + + public boolean isEnglishWithHkb() { + return MODE_HKB_ENGLISH == mInputMode; + } + + public boolean isEnglishWithSkb() { + return MODE_SKB_ENGLISH_LOWER == mInputMode + || MODE_SKB_ENGLISH_UPPER == mInputMode; + } + + public boolean isEnglishUpperCaseWithSkb() { + return MODE_SKB_ENGLISH_UPPER == mInputMode; + } + + public boolean isChineseText() { + int skbLayout = (mInputMode & MASK_SKB_LAYOUT); + if (MASK_SKB_LAYOUT_QWERTY == skbLayout || 0 == skbLayout) { + int language = (mInputMode & MASK_LANGUAGE); + if (MASK_LANGUAGE_CN == language) return true; + } + return false; + } + + public boolean isChineseTextWithHkb() { + int skbLayout = (mInputMode & MASK_SKB_LAYOUT); + if (0 == skbLayout) { + int language = (mInputMode & MASK_LANGUAGE); + if (MASK_LANGUAGE_CN == language) return true; + } + return false; + } + + public boolean isChineseTextWithSkb() { + int skbLayout = (mInputMode & MASK_SKB_LAYOUT); + if (MASK_SKB_LAYOUT_QWERTY == skbLayout) { + int language = (mInputMode & MASK_LANGUAGE); + if (MASK_LANGUAGE_CN == language) return true; + } + return false; + } + + public boolean isSymbolWithSkb() { + int skbLayout = (mInputMode & MASK_SKB_LAYOUT); + if (MASK_SKB_LAYOUT_SYMBOL1 == skbLayout + || MASK_SKB_LAYOUT_SYMBOL2 == skbLayout) { + return true; + } + return false; + } + + public boolean isEnterNoramlState() { + return mEnterKeyNormal; + } + + public boolean tryHandleLongPressSwitch(int keyCode) { + if (USERDEF_KEYCODE_LANG_2 == keyCode + || USERDEF_KEYCODE_PHONE_SYM_4 == keyCode) { + mImeService.showOptionsMenu(); + return true; + } + return false; + } + + private void saveInputMode(int newInputMode) { + mPreviousInputMode = mInputMode; + mInputMode = newInputMode; + + int skbLayout = (mInputMode & MASK_SKB_LAYOUT); + if (MASK_SKB_LAYOUT_QWERTY == skbLayout || 0 == skbLayout) { + mRecentLauageInputMode = mInputMode; + } + + mInputIcon = R.drawable.ime_pinyin; + if (isEnglishWithHkb()) { + mInputIcon = R.drawable.ime_en; + } else if (isChineseTextWithHkb()) { + mInputIcon = R.drawable.ime_pinyin; + } + + if (!Environment.getInstance().hasHardKeyboard()) { + mInputIcon = 0; + } + } + + private void prepareToggleStates(boolean needSkb) { + mEnterKeyNormal = true; + if (!needSkb) return; + + mToggleStates.mQwerty = false; + mToggleStates.mKeyStatesNum = 0; + + int states[] = mToggleStates.mKeyStates; + int statesNum = 0; + // Toggle state for language. + int language = (mInputMode & MASK_LANGUAGE); + int layout = (mInputMode & MASK_SKB_LAYOUT); + int charcase = (mInputMode & MASK_CASE); + int variation = mEditorInfo.inputType & EditorInfo.TYPE_MASK_VARIATION; + + if (MASK_SKB_LAYOUT_PHONE != layout) { + if (MASK_LANGUAGE_CN == language) { + // Chinese and Chinese symbol are always the default states, + // do not add a toggling operation. + if (MASK_SKB_LAYOUT_QWERTY == layout) { + mToggleStates.mQwerty = true; + mToggleStates.mQwertyUpperCase = true; + if (mShortMessageField) { + states[statesNum] = mToggleStateSmiley; + statesNum++; + } + } + } else if (MASK_LANGUAGE_EN == language) { + if (MASK_SKB_LAYOUT_QWERTY == layout) { + mToggleStates.mQwerty = true; + mToggleStates.mQwertyUpperCase = false; + states[statesNum] = mToggleStateEnLower; + if (MASK_CASE_UPPER == charcase) { + mToggleStates.mQwertyUpperCase = true; + states[statesNum] = mToggleStateEnUpper; + } + statesNum++; + } else if (MASK_SKB_LAYOUT_SYMBOL1 == layout) { + states[statesNum] = mToggleStateEnSym1; + statesNum++; + } else if (MASK_SKB_LAYOUT_SYMBOL2 == layout) { + states[statesNum] = mToggleStateEnSym2; + statesNum++; + } + } + + // Toggle rows for QWERTY. + mToggleStates.mRowIdToEnable = KeyRow.DEFAULT_ROW_ID; + if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) { + mToggleStates.mRowIdToEnable = mToggleRowEmailAddress; + } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_URI) { + mToggleStates.mRowIdToEnable = mToggleRowUri; + } else if (MASK_LANGUAGE_CN == language) { + mToggleStates.mRowIdToEnable = mToggleRowCn; + } else if (MASK_LANGUAGE_EN == language) { + mToggleStates.mRowIdToEnable = mToggleRowEn; + } + } else { + if (MASK_CASE_UPPER == charcase) { + states[statesNum] = mToggleStatePhoneSym; + statesNum++; + } + } + + // Toggle state for enter key. + int action = mEditorInfo.imeOptions + & (EditorInfo.IME_MASK_ACTION | EditorInfo.IME_FLAG_NO_ENTER_ACTION); + + if (action == EditorInfo.IME_ACTION_GO) { + states[statesNum] = mToggleStateGo; + statesNum++; + mEnterKeyNormal = false; + } else if (action == EditorInfo.IME_ACTION_SEARCH) { + states[statesNum] = mToggleStateSearch; + statesNum++; + mEnterKeyNormal = false; + } else if (action == EditorInfo.IME_ACTION_SEND) { + states[statesNum] = mToggleStateSend; + statesNum++; + mEnterKeyNormal = false; + } else if (action == EditorInfo.IME_ACTION_NEXT) { + int f = mEditorInfo.inputType & EditorInfo.TYPE_MASK_FLAGS; + if (f != EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) { + states[statesNum] = mToggleStateNext; + statesNum++; + mEnterKeyNormal = false; + } + } else if (action == EditorInfo.IME_ACTION_DONE) { + states[statesNum] = mToggleStateDone; + statesNum++; + mEnterKeyNormal = false; + } + mToggleStates.mKeyStatesNum = statesNum; + } +} diff --git a/src/com/android/inputmethod/pinyin/KeyMapDream.java b/src/com/android/inputmethod/pinyin/KeyMapDream.java new file mode 100644 index 0000000..5a95c6f --- /dev/null +++ b/src/com/android/inputmethod/pinyin/KeyMapDream.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import android.view.KeyEvent; + +/** + * Class used to map the symbols on Dream's hardware keyboard to corresponding + * Chinese full-width symbols. + */ +public class KeyMapDream { + // Number of shift bits to store full-width symbols + private static final int SHIFT_FWCH = 8; + private static final int[] mKeyMap = { + KeyEvent.KEYCODE_UNKNOWN, + KeyEvent.KEYCODE_SOFT_LEFT, + KeyEvent.KEYCODE_SOFT_RIGHT, + KeyEvent.KEYCODE_HOME, + KeyEvent.KEYCODE_BACK, + KeyEvent.KEYCODE_CALL, + KeyEvent.KEYCODE_ENDCALL, + KeyEvent.KEYCODE_0 | ('\uff09' << SHIFT_FWCH), // ) + KeyEvent.KEYCODE_1 | ('\uff01' << SHIFT_FWCH), // ! + KeyEvent.KEYCODE_2 | ('\uff20' << SHIFT_FWCH), // @ + KeyEvent.KEYCODE_3 | ('\uff03' << SHIFT_FWCH), // # + KeyEvent.KEYCODE_4 | ('\uffe5' << SHIFT_FWCH), // $ - fullwidth Yuan + KeyEvent.KEYCODE_5 | ('\uff05' << SHIFT_FWCH), // % + KeyEvent.KEYCODE_6 | ('\u2026' << SHIFT_FWCH), // ^ - Apostrophe + KeyEvent.KEYCODE_7 | ('\uff06' << SHIFT_FWCH), // & + KeyEvent.KEYCODE_8 | ('\uff0a' << SHIFT_FWCH), // * + KeyEvent.KEYCODE_9 | ('\uff08' << SHIFT_FWCH), // ( + KeyEvent.KEYCODE_STAR, + KeyEvent.KEYCODE_POUND, + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_DPAD_CENTER, + KeyEvent.KEYCODE_VOLUME_UP, + KeyEvent.KEYCODE_VOLUME_DOWN, + KeyEvent.KEYCODE_POWER, + KeyEvent.KEYCODE_CAMERA, + KeyEvent.KEYCODE_CLEAR, + KeyEvent.KEYCODE_A, + KeyEvent.KEYCODE_B | ('\uff3d' << SHIFT_FWCH), // ] + KeyEvent.KEYCODE_C | ('\u00a9' << SHIFT_FWCH), // copyright + KeyEvent.KEYCODE_D | ('\u3001' << SHIFT_FWCH), // \\ + KeyEvent.KEYCODE_E | ('_' << SHIFT_FWCH), // _ + KeyEvent.KEYCODE_F | ('\uff5b' << SHIFT_FWCH), // { + KeyEvent.KEYCODE_G | ('\uff5d' << SHIFT_FWCH), // } + KeyEvent.KEYCODE_H | ('\uff1a' << SHIFT_FWCH), // : + KeyEvent.KEYCODE_I | ('\uff0d' << SHIFT_FWCH), // - + KeyEvent.KEYCODE_J | ('\uff1b' << SHIFT_FWCH), // ; + KeyEvent.KEYCODE_K | ('\u201c' << SHIFT_FWCH), // " + KeyEvent.KEYCODE_L | ('\u2019' << SHIFT_FWCH), // ' + KeyEvent.KEYCODE_M | ('\u300b' << SHIFT_FWCH), // > - French quotes + KeyEvent.KEYCODE_N | ('\u300a' << SHIFT_FWCH), // < - French quotes + KeyEvent.KEYCODE_O | ('\uff0b' << SHIFT_FWCH), // + + KeyEvent.KEYCODE_P | ('\uff1d' << SHIFT_FWCH), // = + KeyEvent.KEYCODE_Q | ('\t' << SHIFT_FWCH), // \t + KeyEvent.KEYCODE_R | ('\u00ae' << SHIFT_FWCH), // trademark + KeyEvent.KEYCODE_S | ('\uff5c' << SHIFT_FWCH), // | + KeyEvent.KEYCODE_T | ('\u20ac' << SHIFT_FWCH), // + KeyEvent.KEYCODE_U | ('\u00d7' << SHIFT_FWCH), // multiplier + KeyEvent.KEYCODE_V | ('\uff3b' << SHIFT_FWCH), // [ + KeyEvent.KEYCODE_W | ('\uff40' << SHIFT_FWCH), // ` + KeyEvent.KEYCODE_X, KeyEvent.KEYCODE_Y | ('\u00f7' << SHIFT_FWCH), + KeyEvent.KEYCODE_Z, + KeyEvent.KEYCODE_COMMA | ('\uff1f' << SHIFT_FWCH), + KeyEvent.KEYCODE_PERIOD | ('\uff0f' << SHIFT_FWCH), + KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_RIGHT, + KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT, + KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_SYM, + KeyEvent.KEYCODE_EXPLORER, KeyEvent.KEYCODE_ENVELOPE, + KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_DEL, + KeyEvent.KEYCODE_GRAVE, KeyEvent.KEYCODE_MINUS, + KeyEvent.KEYCODE_EQUALS, KeyEvent.KEYCODE_LEFT_BRACKET, + KeyEvent.KEYCODE_RIGHT_BRACKET, KeyEvent.KEYCODE_BACKSLASH, + KeyEvent.KEYCODE_SEMICOLON, KeyEvent.KEYCODE_APOSTROPHE, + KeyEvent.KEYCODE_SLASH, + KeyEvent.KEYCODE_AT | ('\uff5e' << SHIFT_FWCH), + KeyEvent.KEYCODE_NUM, KeyEvent.KEYCODE_HEADSETHOOK, + KeyEvent.KEYCODE_FOCUS, KeyEvent.KEYCODE_PLUS, + KeyEvent.KEYCODE_MENU, KeyEvent.KEYCODE_NOTIFICATION, + KeyEvent.KEYCODE_SEARCH,}; + + static public char getChineseLabel(int keyCode) { + if (keyCode <= 0 || keyCode >= KeyEvent.getMaxKeyCode()) return 0; + assert ((mKeyMap[keyCode] & 0x000000ff) == keyCode); + return (char) (mKeyMap[keyCode] >> SHIFT_FWCH); + } +} diff --git a/src/com/android/inputmethod/pinyin/PinyinDecoderService.java b/src/com/android/inputmethod/pinyin/PinyinDecoderService.java new file mode 100644 index 0000000..a4a3ac4 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/PinyinDecoderService.java @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import com.android.inputmethod.pinyin.IPinyinDecoderService; + +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; +import java.util.Vector; + +import android.app.Service; +import android.content.Intent; +import android.content.res.AssetFileDescriptor; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +/** + * This class is used to separate the input method kernel in an individual + * service so that both IME and IME-syncer can use it. + */ +public class PinyinDecoderService extends Service { + native static boolean nativeImOpenDecoder(byte fn_sys_dict[], + byte fn_usr_dict[]); + + native static boolean nativeImOpenDecoderFd(FileDescriptor fd, + long startOffset, long length, byte fn_usr_dict[]); + + native static void nativeImSetMaxLens(int maxSpsLen, int maxHzsLen); + + native static boolean nativeImCloseDecoder(); + + native static int nativeImSearch(byte pyBuf[], int pyLen); + + native static int nativeImDelSearch(int pos, boolean is_pos_in_splid, + boolean clear_fixed_this_step); + + native static void nativeImResetSearch(); + + native static int nativeImAddLetter(byte ch); + + native static String nativeImGetPyStr(boolean decoded); + + native static int nativeImGetPyStrLen(boolean decoded); + + native static int[] nativeImGetSplStart(); + + native static String nativeImGetChoice(int choiceId); + + native static int nativeImChoose(int choiceId); + + native static int nativeImCancelLastChoice(); + + native static int nativeImGetFixedLen(); + + native static boolean nativeImCancelInput(); + + native static boolean nativeImFlushCache(); + + native static int nativeImGetPredictsNum(String fixedStr); + + native static String nativeImGetPredictItem(int predictNo); + + // Sync related + native static String nativeSyncUserDict(byte[] user_dict, String tomerge); + + native static boolean nativeSyncBegin(byte[] user_dict); + + native static boolean nativeSyncFinish(); + + native static String nativeSyncGetLemmas(); + + native static int nativeSyncPutLemmas(String tomerge); + + native static int nativeSyncGetLastCount(); + + native static int nativeSyncGetTotalCount(); + + native static boolean nativeSyncClearLastGot(); + + native static int nativeSyncGetCapacity(); + + private final static int MAX_PATH_FILE_LENGTH = 100; + private static boolean inited = false; + + private String mUsr_dict_file; + + static { + try { + System.loadLibrary("jni_pinyinime"); + } catch (UnsatisfiedLinkError ule) { + Log.e("PinyinDecoderService", + "WARNING: Could not load jni_pinyinime natives"); + } + } + + // Get file name of the specified dictionary + private boolean getUsrDictFileName(byte usr_dict[]) { + if (null == usr_dict) { + return false; + } + + for (int i = 0; i < mUsr_dict_file.length(); i++) + usr_dict[i] = (byte) mUsr_dict_file.charAt(i); + usr_dict[mUsr_dict_file.length()] = 0; + + return true; + } + + private void initPinyinEngine() { + byte usr_dict[]; + usr_dict = new byte[MAX_PATH_FILE_LENGTH]; + + // Here is how we open a built-in dictionary for access through + // a file descriptor... + AssetFileDescriptor afd = getResources().openRawResourceFd( + R.raw.dict_pinyin); + if (Environment.getInstance().needDebug()) { + Log + .i("foo", "Dict: start=" + afd.getStartOffset() + + ", length=" + afd.getLength() + ", fd=" + + afd.getParcelFileDescriptor()); + } + if (getUsrDictFileName(usr_dict)) { + inited = nativeImOpenDecoderFd(afd.getFileDescriptor(), afd + .getStartOffset(), afd.getLength(), usr_dict); + } + try { + afd.close(); + } catch (IOException e) { + } + } + + @Override + public void onCreate() { + super.onCreate(); + mUsr_dict_file = getFileStreamPath("usr_dict.dat").getPath(); + // This is a hack to make sure our "files" directory has been + // created. + try { + openFileOutput("dummy", 0).close(); + } catch (FileNotFoundException e) { + } catch (IOException e) { + } + + initPinyinEngine(); + } + + @Override + public void onDestroy() { + nativeImCloseDecoder(); + inited = false; + super.onDestroy(); + } + + private final IPinyinDecoderService.Stub mBinder = new IPinyinDecoderService.Stub() { + public int getInt() { + return 12345; + } + + public void setMaxLens(int maxSpsLen, int maxHzsLen) { + nativeImSetMaxLens(maxSpsLen, maxHzsLen); + } + + public int imSearch(byte[] pyBuf, int pyLen) { + return nativeImSearch(pyBuf, pyLen); + } + + public int imDelSearch(int pos, boolean is_pos_in_splid, + boolean clear_fixed_this_step) { + return nativeImDelSearch(pos, is_pos_in_splid, + clear_fixed_this_step); + } + + public void imResetSearch() { + nativeImResetSearch(); + } + + public int imAddLetter(byte ch) { + return nativeImAddLetter(ch); + } + + public String imGetPyStr(boolean decoded) { + return nativeImGetPyStr(decoded); + } + + public int imGetPyStrLen(boolean decoded) { + return nativeImGetPyStrLen(decoded); + } + + public int[] imGetSplStart() { + return nativeImGetSplStart(); + } + + public String imGetChoice(int choiceId) { + return nativeImGetChoice(choiceId); + } + + public String imGetChoices(int choicesNum) { + String retStr = null; + for (int i = 0; i < choicesNum; i++) { + if (null == retStr) + retStr = nativeImGetChoice(i); + else + retStr += " " + nativeImGetChoice(i); + } + return retStr; + } + + public List<String> imGetChoiceList(int choicesStart, int choicesNum, + int sentFixedLen) { + Vector<String> choiceList = new Vector<String>(); + for (int i = choicesStart; i < choicesStart + choicesNum; i++) { + String retStr = nativeImGetChoice(i); + if (0 == i) retStr = retStr.substring(sentFixedLen); + choiceList.add(retStr); + } + return choiceList; + } + + public int imChoose(int choiceId) { + return nativeImChoose(choiceId); + } + + public int imCancelLastChoice() { + return nativeImCancelLastChoice(); + } + + public int imGetFixedLen() { + return nativeImGetFixedLen(); + } + + public boolean imCancelInput() { + return nativeImCancelInput(); + } + + public void imFlushCache() { + nativeImFlushCache(); + } + + public int imGetPredictsNum(String fixedStr) { + return nativeImGetPredictsNum(fixedStr); + } + + public String imGetPredictItem(int predictNo) { + return nativeImGetPredictItem(predictNo); + } + + public List<String> imGetPredictList(int predictsStart, int predictsNum) { + Vector<String> predictList = new Vector<String>(); + for (int i = predictsStart; i < predictsStart + predictsNum; i++) { + predictList.add(nativeImGetPredictItem(i)); + } + return predictList; + } + + public String syncUserDict(String tomerge) { + byte usr_dict[]; + usr_dict = new byte[MAX_PATH_FILE_LENGTH]; + + if (getUsrDictFileName(usr_dict)) { + return nativeSyncUserDict(usr_dict, tomerge); + } + return null; + } + + public boolean syncBegin() { + byte usr_dict[]; + usr_dict = new byte[MAX_PATH_FILE_LENGTH]; + + if (getUsrDictFileName(usr_dict)) { + return nativeSyncBegin(usr_dict); + } + return false; + } + + public void syncFinish() { + nativeSyncFinish(); + } + + public int syncPutLemmas(String tomerge) { + return nativeSyncPutLemmas(tomerge); + } + + public String syncGetLemmas() { + return nativeSyncGetLemmas(); + } + + public int syncGetLastCount() { + return nativeSyncGetLastCount(); + } + + public int syncGetTotalCount() { + return nativeSyncGetTotalCount(); + } + + public void syncClearLastGot() { + nativeSyncClearLastGot(); + } + + public int imSyncGetCapacity() { + return nativeSyncGetCapacity(); + } + }; + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } +} diff --git a/src/com/android/inputmethod/pinyin/PinyinIME.java b/src/com/android/inputmethod/pinyin/PinyinIME.java new file mode 100644 index 0000000..9ac2c2d --- /dev/null +++ b/src/com/android/inputmethod/pinyin/PinyinIME.java @@ -0,0 +1,2134 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.res.Configuration; +import android.inputmethodservice.InputMethodService; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.preference.PreferenceManager; +import android.util.Log; +import android.view.Gravity; +import android.view.GestureDetector; +import android.view.LayoutInflater; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.view.View.MeasureSpec; +import android.view.ViewGroup.LayoutParams; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.LinearLayout; +import android.widget.PopupWindow; + +import java.util.ArrayList; +import java.util.List; +import java.util.Vector; + +/** + * Main class of the Pinyin input method. + */ +public class PinyinIME extends InputMethodService { + /** + * TAG for debug. + */ + static final String TAG = "PinyinIME"; + + /** + * If is is true, IME will simulate key events for delete key, and send the + * events back to the application. + */ + private static final boolean SIMULATE_KEY_DELETE = true; + + /** + * Necessary environment configurations like screen size for this IME. + */ + private Environment mEnvironment; + + /** + * Used to switch input mode. + */ + private InputModeSwitcher mInputModeSwitcher; + + /** + * Soft keyboard container view to host real soft keyboard view. + */ + private SkbContainer mSkbContainer; + + /** + * The floating container which contains the composing view. If necessary, + * some other view like candiates container can also be put here. + */ + private LinearLayout mFloatingContainer; + + /** + * View to show the composing string. + */ + private ComposingView mComposingView; + + /** + * Window to show the composing string. + */ + private PopupWindow mFloatingWindow; + + /** + * Used to show the floating window. + */ + private PopupTimer mFloatingWindowTimer = new PopupTimer(); + + /** + * View to show candidates list. + */ + private CandidatesContainer mCandidatesContainer; + + /** + * Balloon used when user presses a candidate. + */ + private BalloonHint mCandidatesBalloon; + + /** + * Used to notify the input method when the user touch a candidate. + */ + private ChoiceNotifier mChoiceNotifier; + + /** + * Used to notify gestures from soft keyboard. + */ + private OnGestureListener mGestureListenerSkb; + + /** + * Used to notify gestures from candidates view. + */ + private OnGestureListener mGestureListenerCandidates; + + /** + * The on-screen movement gesture detector for soft keyboard. + */ + private GestureDetector mGestureDetectorSkb; + + /** + * The on-screen movement gesture detector for candidates view. + */ + private GestureDetector mGestureDetectorCandidates; + + /** + * Option dialog to choose settings and other IMEs. + */ + private AlertDialog mOptionsDialog; + + /** + * Connection used to bind the decoding service. + */ + private PinyinDecoderServiceConnection mPinyinDecoderServiceConnection; + + /** + * The current IME status. + * + * @see com.android.inputmethod.pinyin.PinyinIME.ImeState + */ + private ImeState mImeState = ImeState.STATE_IDLE; + + /** + * The decoding information, include spelling(Pinyin) string, decoding + * result, etc. + */ + private DecodingInfo mDecInfo = new DecodingInfo(); + + /** + * For English input. + */ + private EnglishInputProcessor mImEn; + + // receive ringer mode changes + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + SoundManager.getInstance(context).updateRingerMode(); + } + }; + + @Override + public void onCreate() { + mEnvironment = Environment.getInstance(); + if (mEnvironment.needDebug()) { + Log.d(TAG, "onCreate."); + } + super.onCreate(); + + startPinyinDecoderService(); + mImEn = new EnglishInputProcessor(); + Settings.getInstance(PreferenceManager + .getDefaultSharedPreferences(getApplicationContext())); + + mInputModeSwitcher = new InputModeSwitcher(this); + mChoiceNotifier = new ChoiceNotifier(this); + mGestureListenerSkb = new OnGestureListener(false); + mGestureListenerCandidates = new OnGestureListener(true); + mGestureDetectorSkb = new GestureDetector(this, mGestureListenerSkb); + mGestureDetectorCandidates = new GestureDetector(this, + mGestureListenerCandidates); + + mEnvironment.onConfigurationChanged(getResources().getConfiguration(), + this); + } + + @Override + public void onDestroy() { + if (mEnvironment.needDebug()) { + Log.d(TAG, "onDestroy."); + } + unbindService(mPinyinDecoderServiceConnection); + Settings.releaseInstance(); + super.onDestroy(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + Environment env = Environment.getInstance(); + if (mEnvironment.needDebug()) { + Log.d(TAG, "onConfigurationChanged"); + Log.d(TAG, "--last config: " + env.getConfiguration().toString()); + Log.d(TAG, "---new config: " + newConfig.toString()); + } + // We need to change the local environment first so that UI components + // can get the environment instance to handle size issues. When + // super.onConfigurationChanged() is called, onCreateCandidatesView() + // and onCreateInputView() will be executed if necessary. + env.onConfigurationChanged(newConfig, this); + + // Clear related UI of the previous configuration. + if (null != mSkbContainer) { + mSkbContainer.dismissPopups(); + } + if (null != mCandidatesBalloon) { + mCandidatesBalloon.dismiss(); + } + super.onConfigurationChanged(newConfig); + resetToIdleState(false); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (processKey(event, 0 != event.getRepeatCount())) return true; + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (processKey(event, true)) return true; + return super.onKeyUp(keyCode, event); + } + + private boolean processKey(KeyEvent event, boolean realAction) { + if (ImeState.STATE_BYPASS == mImeState) return false; + + int keyCode = event.getKeyCode(); + // SHIFT-SPACE is used to switch between Chinese and English + // when HKB is on. + if (KeyEvent.KEYCODE_SPACE == keyCode && event.isShiftPressed()) { + if (!realAction) return true; + + updateIcon(mInputModeSwitcher.switchLanguageWithHkb()); + resetToIdleState(false); + + int allMetaState = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON + | KeyEvent.META_ALT_RIGHT_ON | KeyEvent.META_SHIFT_ON + | KeyEvent.META_SHIFT_LEFT_ON + | KeyEvent.META_SHIFT_RIGHT_ON | KeyEvent.META_SYM_ON; + getCurrentInputConnection().clearMetaKeyStates(allMetaState); + return true; + } + + // If HKB is on to input English, by-pass the key event so that + // default key listener will handle it. + if (mInputModeSwitcher.isEnglishWithHkb()) { + return false; + } + + if (processFunctionKeys(keyCode, realAction)) { + return true; + } + + int keyChar = 0; + if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) { + keyChar = keyCode - KeyEvent.KEYCODE_A + 'a'; + } else if (keyCode >= KeyEvent.KEYCODE_0 + && keyCode <= KeyEvent.KEYCODE_9) { + keyChar = keyCode - KeyEvent.KEYCODE_0 + '0'; + } else if (keyCode == KeyEvent.KEYCODE_COMMA) { + keyChar = ','; + } else if (keyCode == KeyEvent.KEYCODE_PERIOD) { + keyChar = '.'; + } else if (keyCode == KeyEvent.KEYCODE_SPACE) { + keyChar = ' '; + } else if (keyCode == KeyEvent.KEYCODE_APOSTROPHE) { + keyChar = '\''; + } + + if (mInputModeSwitcher.isEnglishWithSkb()) { + return mImEn.processKey(getCurrentInputConnection(), event, + mInputModeSwitcher.isEnglishUpperCaseWithSkb(), realAction); + } else if (mInputModeSwitcher.isChineseText()) { + if (mImeState == ImeState.STATE_IDLE || + mImeState == ImeState.STATE_APP_COMPLETION) { + mImeState = ImeState.STATE_IDLE; + return processStateIdle(keyChar, keyCode, event, realAction); + } else if (mImeState == ImeState.STATE_INPUT) { + return processStateInput(keyChar, keyCode, event, realAction); + } else if (mImeState == ImeState.STATE_PREDICT) { + return processStatePredict(keyChar, keyCode, event, realAction); + } else if (mImeState == ImeState.STATE_COMPOSING) { + return processStateEditComposing(keyChar, keyCode, event, + realAction); + } + } else { + if (0 != keyChar && realAction) { + commitResultText(String.valueOf((char) keyChar)); + } + } + + return false; + } + + // keyCode can be from both hard key or soft key. + private boolean processFunctionKeys(int keyCode, boolean realAction) { + // Back key is used to dismiss all popup UI in a soft keyboard. + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (isInputViewShown()) { + if (mSkbContainer.handleBack(realAction)) return true; + } + } + + // Chinese related input is handle separately. + if (mInputModeSwitcher.isChineseText()) { + return false; + } + + if (null != mCandidatesContainer && mCandidatesContainer.isShown() + && !mDecInfo.isCandidatesListEmpty()) { + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { + if (!realAction) return true; + + chooseCandidate(-1); + return true; + } + + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { + if (!realAction) return true; + mCandidatesContainer.activeCurseBackward(); + return true; + } + + if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + if (!realAction) return true; + mCandidatesContainer.activeCurseForward(); + return true; + } + + if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { + if (!realAction) return true; + mCandidatesContainer.pageBackward(false, true); + return true; + } + + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + if (!realAction) return true; + mCandidatesContainer.pageForward(false, true); + return true; + } + + if (keyCode == KeyEvent.KEYCODE_DEL && + ImeState.STATE_PREDICT == mImeState) { + if (!realAction) return true; + resetToIdleState(false); + return true; + } + } else { + if (keyCode == KeyEvent.KEYCODE_DEL) { + if (!realAction) return true; + if (SIMULATE_KEY_DELETE) { + simulateKeyEventDownUp(keyCode); + } else { + getCurrentInputConnection().deleteSurroundingText(1, 0); + } + return true; + } + if (keyCode == KeyEvent.KEYCODE_ENTER) { + if (!realAction) return true; + sendKeyChar('\n'); + return true; + } + if (keyCode == KeyEvent.KEYCODE_SPACE) { + if (!realAction) return true; + sendKeyChar(' '); + return true; + } + } + + return false; + } + + private boolean processStateIdle(int keyChar, int keyCode, KeyEvent event, + boolean realAction) { + // In this status, when user presses keys in [a..z], the status will + // change to input state. + if (keyChar >= 'a' && keyChar <= 'z' && !event.isAltPressed()) { + if (!realAction) return true; + mDecInfo.addSplChar((char) keyChar, true); + chooseAndUpdate(-1); + return true; + } else if (keyCode == KeyEvent.KEYCODE_DEL) { + if (!realAction) return true; + if (SIMULATE_KEY_DELETE) { + simulateKeyEventDownUp(keyCode); + } else { + getCurrentInputConnection().deleteSurroundingText(1, 0); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_ENTER) { + if (!realAction) return true; + sendKeyChar('\n'); + return true; + } else if (keyCode == KeyEvent.KEYCODE_ALT_LEFT + || keyCode == KeyEvent.KEYCODE_ALT_RIGHT + || keyCode == KeyEvent.KEYCODE_SHIFT_LEFT + || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { + return true; + } else if (event.isAltPressed()) { + char fullwidth_char = KeyMapDream.getChineseLabel(keyCode); + if (0 != fullwidth_char) { + if (realAction) { + String result = String.valueOf(fullwidth_char); + commitResultText(result); + } + return true; + } else { + if (keyCode >= KeyEvent.KEYCODE_A + && keyCode <= KeyEvent.KEYCODE_Z) { + return true; + } + } + } else if (keyChar != 0 && keyChar != '\t') { + if (realAction) { + if (keyChar == ',' || keyChar == '.') { + inputCommaPeriod("", keyChar, false, ImeState.STATE_IDLE); + } else { + if (0 != keyChar) { + String result = String.valueOf((char) keyChar); + commitResultText(result); + } + } + } + return true; + } + return false; + } + + private boolean processStateInput(int keyChar, int keyCode, KeyEvent event, + boolean realAction) { + // If ALT key is pressed, input alternative key. But if the + // alternative key is quote key, it will be used for input a splitter + // in Pinyin string. + if (event.isAltPressed()) { + if ('\'' != event.getUnicodeChar(event.getMetaState())) { + if (realAction) { + char fullwidth_char = KeyMapDream.getChineseLabel(keyCode); + if (0 != fullwidth_char) { + commitResultText(mDecInfo + .getCurrentFullSent(mCandidatesContainer + .getActiveCandiatePos()) + + String.valueOf(fullwidth_char)); + resetToIdleState(false); + } + } + return true; + } else { + keyChar = '\''; + } + } + + if (keyChar >= 'a' && keyChar <= 'z' || keyChar == '\'' + && !mDecInfo.charBeforeCursorIsSeparator() + || keyCode == KeyEvent.KEYCODE_DEL) { + if (!realAction) return true; + return processSurfaceChange(keyChar, keyCode); + } else if (keyChar == ',' || keyChar == '.') { + if (!realAction) return true; + inputCommaPeriod(mDecInfo.getCurrentFullSent(mCandidatesContainer + .getActiveCandiatePos()), keyChar, true, + ImeState.STATE_IDLE); + return true; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN + || keyCode == KeyEvent.KEYCODE_DPAD_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + if (!realAction) return true; + + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { + mCandidatesContainer.activeCurseBackward(); + } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + mCandidatesContainer.activeCurseForward(); + } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { + // If it has been the first page, a up key will shift + // the state to edit composing string. + if (!mCandidatesContainer.pageBackward(false, true)) { + mCandidatesContainer.enableActiveHighlight(false); + changeToStateComposing(true); + updateComposingText(true); + } + } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + mCandidatesContainer.pageForward(false, true); + } + return true; + } else if (keyCode >= KeyEvent.KEYCODE_1 + && keyCode <= KeyEvent.KEYCODE_9) { + if (!realAction) return true; + + int activePos = keyCode - KeyEvent.KEYCODE_1; + int currentPage = mCandidatesContainer.getCurrentPage(); + if (activePos < mDecInfo.getCurrentPageSize(currentPage)) { + activePos = activePos + + mDecInfo.getCurrentPageStart(currentPage); + if (activePos >= 0) { + chooseAndUpdate(activePos); + } + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_ENTER) { + if (!realAction) return true; + if (mInputModeSwitcher.isEnterNoramlState()) { + commitResultText(mDecInfo.getOrigianlSplStr().toString()); + resetToIdleState(false); + } else { + commitResultText(mDecInfo + .getCurrentFullSent(mCandidatesContainer + .getActiveCandiatePos())); + sendKeyChar('\n'); + resetToIdleState(false); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER + || keyCode == KeyEvent.KEYCODE_SPACE) { + if (!realAction) return true; + chooseCandidate(-1); + return true; + } else if (keyCode == KeyEvent.KEYCODE_BACK) { + if (!realAction) return true; + resetToIdleState(false); + requestHideSelf(0); + return true; + } + return false; + } + + private boolean processStatePredict(int keyChar, int keyCode, + KeyEvent event, boolean realAction) { + if (!realAction) return true; + + // If ALT key is pressed, input alternative key. + if (event.isAltPressed()) { + char fullwidth_char = KeyMapDream.getChineseLabel(keyCode); + if (0 != fullwidth_char) { + commitResultText(mDecInfo.getCandidate(mCandidatesContainer + .getActiveCandiatePos()) + + String.valueOf(fullwidth_char)); + resetToIdleState(false); + } + return true; + } + + // In this status, when user presses keys in [a..z], the status will + // change to input state. + if (keyChar >= 'a' && keyChar <= 'z') { + changeToStateInput(true); + mDecInfo.addSplChar((char) keyChar, true); + chooseAndUpdate(-1); + } else if (keyChar == ',' || keyChar == '.') { + inputCommaPeriod("", keyChar, true, ImeState.STATE_IDLE); + } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN + || keyCode == KeyEvent.KEYCODE_DPAD_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { + mCandidatesContainer.activeCurseBackward(); + } + if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + mCandidatesContainer.activeCurseForward(); + } + if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { + mCandidatesContainer.pageBackward(false, true); + } + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + mCandidatesContainer.pageForward(false, true); + } + } else if (keyCode == KeyEvent.KEYCODE_DEL) { + resetToIdleState(false); + } else if (keyCode == KeyEvent.KEYCODE_BACK) { + resetToIdleState(false); + requestHideSelf(0); + } else if (keyCode >= KeyEvent.KEYCODE_1 + && keyCode <= KeyEvent.KEYCODE_9) { + int activePos = keyCode - KeyEvent.KEYCODE_1; + int currentPage = mCandidatesContainer.getCurrentPage(); + if (activePos < mDecInfo.getCurrentPageSize(currentPage)) { + activePos = activePos + + mDecInfo.getCurrentPageStart(currentPage); + if (activePos >= 0) { + chooseAndUpdate(activePos); + } + } + } else if (keyCode == KeyEvent.KEYCODE_ENTER) { + sendKeyChar('\n'); + resetToIdleState(false); + } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER + || keyCode == KeyEvent.KEYCODE_SPACE) { + chooseCandidate(-1); + } + + return true; + } + + private boolean processStateEditComposing(int keyChar, int keyCode, + KeyEvent event, boolean realAction) { + if (!realAction) return true; + + ComposingView.ComposingStatus cmpsvStatus = + mComposingView.getComposingStatus(); + + // If ALT key is pressed, input alternative key. But if the + // alternative key is quote key, it will be used for input a splitter + // in Pinyin string. + if (event.isAltPressed()) { + if ('\'' != event.getUnicodeChar(event.getMetaState())) { + char fullwidth_char = KeyMapDream.getChineseLabel(keyCode); + if (0 != fullwidth_char) { + String retStr; + if (ComposingView.ComposingStatus.SHOW_STRING_LOWERCASE == + cmpsvStatus) { + retStr = mDecInfo.getOrigianlSplStr().toString(); + } else { + retStr = mDecInfo.getComposingStr(); + } + commitResultText(retStr + String.valueOf(fullwidth_char)); + resetToIdleState(false); + } + return true; + } else { + keyChar = '\''; + } + } + + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + if (!mDecInfo.selectionFinished()) { + changeToStateInput(true); + } + } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + mComposingView.moveCursor(keyCode); + } else if ((keyCode == KeyEvent.KEYCODE_ENTER && mInputModeSwitcher + .isEnterNoramlState()) + || keyCode == KeyEvent.KEYCODE_DPAD_CENTER + || keyCode == KeyEvent.KEYCODE_SPACE) { + if (ComposingView.ComposingStatus.SHOW_STRING_LOWERCASE == cmpsvStatus) { + String str = mDecInfo.getOrigianlSplStr().toString(); + if (!tryInputRawUnicode(str)) { + commitResultText(str); + } + } else if (ComposingView.ComposingStatus.EDIT_PINYIN == cmpsvStatus) { + String str = mDecInfo.getComposingStr(); + if (!tryInputRawUnicode(str)) { + commitResultText(str); + } + } else { + commitResultText(mDecInfo.getComposingStr()); + } + resetToIdleState(false); + } else if (keyCode == KeyEvent.KEYCODE_ENTER + && !mInputModeSwitcher.isEnterNoramlState()) { + String retStr; + if (!mDecInfo.isCandidatesListEmpty()) { + retStr = mDecInfo.getCurrentFullSent(mCandidatesContainer + .getActiveCandiatePos()); + } else { + retStr = mDecInfo.getComposingStr(); + } + commitResultText(retStr); + sendKeyChar('\n'); + resetToIdleState(false); + } else if (keyCode == KeyEvent.KEYCODE_BACK) { + resetToIdleState(false); + requestHideSelf(0); + return true; + } else { + return processSurfaceChange(keyChar, keyCode); + } + return true; + } + + private boolean tryInputRawUnicode(String str) { + if (str.length() > 7) { + if (str.substring(0, 7).compareTo("unicode") == 0) { + try { + String digitStr = str.substring(7); + int startPos = 0; + int radix = 10; + if (digitStr.length() > 2 && digitStr.charAt(0) == '0' + && digitStr.charAt(1) == 'x') { + startPos = 2; + radix = 16; + } + digitStr = digitStr.substring(startPos); + int unicode = Integer.parseInt(digitStr, radix); + if (unicode > 0) { + char low = (char) (unicode & 0x0000ffff); + char high = (char) ((unicode & 0xffff0000) >> 16); + commitResultText(String.valueOf(low)); + if (0 != high) { + commitResultText(String.valueOf(high)); + } + } + return true; + } catch (NumberFormatException e) { + return false; + } + } else if (str.substring(str.length() - 7, str.length()).compareTo( + "unicode") == 0) { + String resultStr = ""; + for (int pos = 0; pos < str.length() - 7; pos++) { + if (pos > 0) { + resultStr += " "; + } + + resultStr += "0x" + Integer.toHexString(str.charAt(pos)); + } + commitResultText(String.valueOf(resultStr)); + return true; + } + } + return false; + } + + private boolean processSurfaceChange(int keyChar, int keyCode) { + if (mDecInfo.isSplStrFull() && KeyEvent.KEYCODE_DEL != keyCode) { + return true; + } + + if ((keyChar >= 'a' && keyChar <= 'z') + || (keyChar == '\'' && !mDecInfo.charBeforeCursorIsSeparator()) + || (((keyChar >= '0' && keyChar <= '9') || keyChar == ' ') && ImeState.STATE_COMPOSING == mImeState)) { + mDecInfo.addSplChar((char) keyChar, false); + chooseAndUpdate(-1); + } else if (keyCode == KeyEvent.KEYCODE_DEL) { + mDecInfo.prepareDeleteBeforeCursor(); + chooseAndUpdate(-1); + } + return true; + } + + private void changeToStateComposing(boolean updateUi) { + mImeState = ImeState.STATE_COMPOSING; + if (!updateUi) return; + + if (null != mSkbContainer && mSkbContainer.isShown()) { + mSkbContainer.toggleCandidateMode(true); + } + } + + private void changeToStateInput(boolean updateUi) { + mImeState = ImeState.STATE_INPUT; + if (!updateUi) return; + + if (null != mSkbContainer && mSkbContainer.isShown()) { + mSkbContainer.toggleCandidateMode(true); + } + showCandidateWindow(true); + } + + private void simulateKeyEventDownUp(int keyCode) { + InputConnection ic = getCurrentInputConnection(); + if (null == ic) return; + + ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); + ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); + } + + private void commitResultText(String resultText) { + InputConnection ic = getCurrentInputConnection(); + if (null != ic) ic.commitText(resultText, 1); + if (null != mComposingView) { + mComposingView.setVisibility(View.INVISIBLE); + mComposingView.invalidate(); + } + } + + private void updateComposingText(boolean visible) { + if (!visible) { + mComposingView.setVisibility(View.INVISIBLE); + } else { + mComposingView.setDecodingInfo(mDecInfo, mImeState); + mComposingView.setVisibility(View.VISIBLE); + } + mComposingView.invalidate(); + } + + private void inputCommaPeriod(String preEdit, int keyChar, + boolean dismissCandWindow, ImeState nextState) { + if (keyChar == ',') + preEdit += '\uff0c'; + else if (keyChar == '.') + preEdit += '\u3002'; + else + return; + commitResultText(preEdit); + if (dismissCandWindow) resetCandidateWindow(); + mImeState = nextState; + } + + private void resetToIdleState(boolean resetInlineText) { + if (ImeState.STATE_IDLE == mImeState) return; + + mImeState = ImeState.STATE_IDLE; + mDecInfo.reset(); + + if (null != mComposingView) mComposingView.reset(); + if (resetInlineText) commitResultText(""); + resetCandidateWindow(); + } + + private void chooseAndUpdate(int candId) { + if (!mInputModeSwitcher.isChineseText()) { + String choice = mDecInfo.getCandidate(candId); + if (null != choice) { + commitResultText(choice); + } + resetToIdleState(false); + return; + } + + if (ImeState.STATE_PREDICT != mImeState) { + // Get result candidate list, if choice_id < 0, do a new decoding. + // If choice_id >=0, select the candidate, and get the new candidate + // list. + mDecInfo.chooseDecodingCandidate(candId); + } else { + // Choose a prediction item. + mDecInfo.choosePredictChoice(candId); + } + + if (mDecInfo.getComposingStr().length() > 0) { + String resultStr; + resultStr = mDecInfo.getComposingStrActivePart(); + + // choiceId >= 0 means user finishes a choice selection. + if (candId >= 0 && mDecInfo.canDoPrediction()) { + commitResultText(resultStr); + mImeState = ImeState.STATE_PREDICT; + if (null != mSkbContainer && mSkbContainer.isShown()) { + mSkbContainer.toggleCandidateMode(false); + } + // Try to get the prediction list. + if (Settings.getPrediction()) { + InputConnection ic = getCurrentInputConnection(); + if (null != ic) { + CharSequence cs = ic.getTextBeforeCursor(3, 0); + if (null != cs) { + mDecInfo.preparePredicts(cs); + } + } + } else { + mDecInfo.resetCandidates(); + } + + if (mDecInfo.mCandidatesList.size() > 0) { + showCandidateWindow(false); + } else { + resetToIdleState(false); + } + } else { + if (ImeState.STATE_IDLE == mImeState) { + if (mDecInfo.getSplStrDecodedLen() == 0) { + changeToStateComposing(true); + } else { + changeToStateInput(true); + } + } else { + if (mDecInfo.selectionFinished()) { + changeToStateComposing(true); + } + } + showCandidateWindow(true); + } + } else { + resetToIdleState(false); + } + } + + // If activeCandNo is less than 0, get the current active candidate number + // from candidate view, otherwise use activeCandNo. + private void chooseCandidate(int activeCandNo) { + if (activeCandNo < 0) { + activeCandNo = mCandidatesContainer.getActiveCandiatePos(); + } + if (activeCandNo >= 0) { + chooseAndUpdate(activeCandNo); + } + } + + private boolean startPinyinDecoderService() { + if (null == mDecInfo.mIPinyinDecoderService) { + Intent serviceIntent = new Intent(); + serviceIntent.setClass(this, PinyinDecoderService.class); + + if (null == mPinyinDecoderServiceConnection) { + mPinyinDecoderServiceConnection = new PinyinDecoderServiceConnection(); + } + + // Bind service + if (bindService(serviceIntent, mPinyinDecoderServiceConnection, + Context.BIND_AUTO_CREATE)) { + return true; + } else { + return false; + } + } + return true; + } + + @Override + public View onCreateCandidatesView() { + if (mEnvironment.needDebug()) { + Log.d(TAG, "onCreateCandidatesView."); + } + + LayoutInflater inflater = getLayoutInflater(); + // Inflate the floating container view + mFloatingContainer = (LinearLayout) inflater.inflate( + R.layout.floating_container, null); + + // The first child is the composing view. + mComposingView = (ComposingView) mFloatingContainer.getChildAt(0); + + mCandidatesContainer = (CandidatesContainer) inflater.inflate( + R.layout.candidates_container, null); + + // Create balloon hint for candidates view. + mCandidatesBalloon = new BalloonHint(this, mCandidatesContainer, + MeasureSpec.UNSPECIFIED); + mCandidatesBalloon.setBalloonBackground(getResources().getDrawable( + R.drawable.candidate_balloon_bg)); + mCandidatesContainer.initialize(mChoiceNotifier, mCandidatesBalloon, + mGestureDetectorCandidates); + + // The floating window + if (null != mFloatingWindow && mFloatingWindow.isShowing()) { + mFloatingWindowTimer.cancelShowing(); + mFloatingWindow.dismiss(); + } + mFloatingWindow = new PopupWindow(this); + mFloatingWindow.setClippingEnabled(false); + mFloatingWindow.setBackgroundDrawable(null); + mFloatingWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + mFloatingWindow.setContentView(mFloatingContainer); + + setCandidatesViewShown(true); + return mCandidatesContainer; + } + + public void responseSoftKeyEvent(SoftKey sKey) { + if (null == sKey) return; + + InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; + + int keyCode = sKey.getKeyCode(); + // Process some general keys, including KEYCODE_DEL, KEYCODE_SPACE, + // KEYCODE_ENTER and KEYCODE_DPAD_CENTER. + if (sKey.isKeyCodeKey()) { + if (processFunctionKeys(keyCode, true)) return; + } + + if (sKey.isUserDefKey()) { + updateIcon(mInputModeSwitcher.switchModeForUserKey(keyCode)); + resetToIdleState(false); + mSkbContainer.updateInputMode(); + } else { + if (sKey.isKeyCodeKey()) { + KeyEvent eDown = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, + keyCode, 0, 0, 0, 0, KeyEvent.FLAG_SOFT_KEYBOARD); + KeyEvent eUp = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, + 0, 0, 0, 0, KeyEvent.FLAG_SOFT_KEYBOARD); + + onKeyDown(keyCode, eDown); + onKeyUp(keyCode, eUp); + } else if (sKey.isUniStrKey()) { + boolean kUsed = false; + String keyLabel = sKey.getKeyLabel(); + if (mInputModeSwitcher.isChineseTextWithSkb() + && (ImeState.STATE_INPUT == mImeState || ImeState.STATE_COMPOSING == mImeState)) { + if (mDecInfo.length() > 0 && keyLabel.length() == 1 + && keyLabel.charAt(0) == '\'') { + processSurfaceChange('\'', 0); + kUsed = true; + } + } + if (!kUsed) { + if (ImeState.STATE_INPUT == mImeState) { + commitResultText(mDecInfo + .getCurrentFullSent(mCandidatesContainer + .getActiveCandiatePos())); + } else if (ImeState.STATE_COMPOSING == mImeState) { + commitResultText(mDecInfo.getComposingStr()); + } + commitResultText(keyLabel); + resetToIdleState(false); + } + } + + // If the current soft keyboard is not sticky, IME needs to go + // back to the previous soft keyboard automatically. + if (!mSkbContainer.isCurrentSkbSticky()) { + updateIcon(mInputModeSwitcher.requestBackToPreviousSkb()); + resetToIdleState(false); + mSkbContainer.updateInputMode(); + } + } + } + + private void showCandidateWindow(boolean showComposingView) { + if (mEnvironment.needDebug()) { + Log.d(TAG, "Candidates window is shown. Parent = " + + mCandidatesContainer); + } + + setCandidatesViewShown(true); + + if (null != mSkbContainer) mSkbContainer.requestLayout(); + + if (null == mCandidatesContainer) { + resetToIdleState(false); + return; + } + + updateComposingText(showComposingView); + mCandidatesContainer.showCandidates(mDecInfo, + ImeState.STATE_COMPOSING != mImeState); + mFloatingWindowTimer.postShowFloatingWindow(); + } + + private void dismissCandidateWindow() { + if (mEnvironment.needDebug()) { + Log.d(TAG, "Candidates window is to be dismissed"); + } + if (null == mCandidatesContainer) return; + try { + mFloatingWindowTimer.cancelShowing(); + mFloatingWindow.dismiss(); + } catch (Exception e) { + Log.e(TAG, "Fail to show the PopupWindow."); + } + setCandidatesViewShown(false); + + if (null != mSkbContainer && mSkbContainer.isShown()) { + mSkbContainer.toggleCandidateMode(false); + } + } + + private void resetCandidateWindow() { + if (mEnvironment.needDebug()) { + Log.d(TAG, "Candidates window is to be reset"); + } + if (null == mCandidatesContainer) return; + try { + mFloatingWindowTimer.cancelShowing(); + mFloatingWindow.dismiss(); + } catch (Exception e) { + Log.e(TAG, "Fail to show the PopupWindow."); + } + + if (null != mSkbContainer && mSkbContainer.isShown()) { + mSkbContainer.toggleCandidateMode(false); + } + + mDecInfo.resetCandidates(); + + if (null != mCandidatesContainer && mCandidatesContainer.isShown()) { + showCandidateWindow(false); + } + } + + private void updateIcon(int iconId) { + if (iconId > 0) { + showStatusIcon(iconId); + } else { + hideStatusIcon(); + } + } + + @Override + public View onCreateInputView() { + if (mEnvironment.needDebug()) { + Log.d(TAG, "onCreateInputView."); + } + LayoutInflater inflater = getLayoutInflater(); + mSkbContainer = (SkbContainer) inflater.inflate(R.layout.skb_container, + null); + mSkbContainer.setService(this); + mSkbContainer.setInputModeSwitcher(mInputModeSwitcher); + mSkbContainer.setGestureDetector(mGestureDetectorSkb); + return mSkbContainer; + } + + @Override + public void onStartInput(EditorInfo editorInfo, boolean restarting) { + if (mEnvironment.needDebug()) { + Log.d(TAG, "onStartInput " + " ccontentType: " + + String.valueOf(editorInfo.inputType) + " Restarting:" + + String.valueOf(restarting)); + } + updateIcon(mInputModeSwitcher.requestInputWithHkb(editorInfo)); + resetToIdleState(false); + } + + @Override + public void onStartInputView(EditorInfo editorInfo, boolean restarting) { + if (mEnvironment.needDebug()) { + Log.d(TAG, "onStartInputView " + " contentType: " + + String.valueOf(editorInfo.inputType) + " Restarting:" + + String.valueOf(restarting)); + } + updateIcon(mInputModeSwitcher.requestInputWithSkb(editorInfo)); + resetToIdleState(false); + mSkbContainer.updateInputMode(); + setCandidatesViewShown(false); + } + + @Override + public void onFinishInputView(boolean finishingInput) { + if (mEnvironment.needDebug()) { + Log.d(TAG, "onFinishInputView."); + } + resetToIdleState(false); + super.onFinishInputView(finishingInput); + } + + @Override + public void onFinishInput() { + if (mEnvironment.needDebug()) { + Log.d(TAG, "onFinishInput."); + } + resetToIdleState(false); + super.onFinishInput(); + } + + @Override + public void onFinishCandidatesView(boolean finishingInput) { + if (mEnvironment.needDebug()) { + Log.d(TAG, "onFinishCandidateView."); + } + resetToIdleState(false); + super.onFinishCandidatesView(finishingInput); + } + + @Override public void onDisplayCompletions(CompletionInfo[] completions) { + if (!isFullscreenMode()) return; + if (null == completions || completions.length <= 0) return; + if (null == mSkbContainer || !mSkbContainer.isShown()) return; + + if (!mInputModeSwitcher.isChineseText() || + ImeState.STATE_IDLE == mImeState || + ImeState.STATE_PREDICT == mImeState) { + mImeState = ImeState.STATE_APP_COMPLETION; + mDecInfo.prepareAppCompletions(completions); + showCandidateWindow(false); + } + } + + private void onChoiceTouched(int activeCandNo) { + if (mImeState == ImeState.STATE_COMPOSING) { + changeToStateInput(true); + } else if (mImeState == ImeState.STATE_INPUT + || mImeState == ImeState.STATE_PREDICT) { + chooseCandidate(activeCandNo); + } else if (mImeState == ImeState.STATE_APP_COMPLETION) { + if (null != mDecInfo.mAppCompletions && activeCandNo >= 0 && + activeCandNo < mDecInfo.mAppCompletions.length) { + CompletionInfo ci = mDecInfo.mAppCompletions[activeCandNo]; + if (null != ci) { + InputConnection ic = getCurrentInputConnection(); + ic.commitCompletion(ci); + } + } + resetToIdleState(false); + } + } + + @Override + public void requestHideSelf(int flags) { + if (mEnvironment.needDebug()) { + Log.d(TAG, "DimissSoftInput."); + } + dismissCandidateWindow(); + if (null != mSkbContainer && mSkbContainer.isShown()) { + mSkbContainer.dismissPopups(); + } + super.requestHideSelf(flags); + } + + public void showOptionsMenu() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setCancelable(true); + builder.setIcon(R.drawable.app_icon); + builder.setNegativeButton(android.R.string.cancel, null); + CharSequence itemSettings = getString(R.string.ime_settings_activity_name); + CharSequence itemInputMethod = getString(com.android.internal.R.string.inputMethod); + builder.setItems(new CharSequence[] {itemSettings, itemInputMethod}, + new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface di, int position) { + di.dismiss(); + switch (position) { + case 0: + launchSettings(); + break; + case 1: + InputMethodManager.getInstance(PinyinIME.this) + .showInputMethodPicker(); + break; + } + } + }); + builder.setTitle(getString(R.string.ime_name)); + mOptionsDialog = builder.create(); + Window window = mOptionsDialog.getWindow(); + WindowManager.LayoutParams lp = window.getAttributes(); + lp.token = mSkbContainer.getWindowToken(); + lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; + window.setAttributes(lp); + window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + mOptionsDialog.show(); + } + + private void launchSettings() { + Intent intent = new Intent(); + intent.setClass(PinyinIME.this, SettingsActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + + private class PopupTimer extends Handler implements Runnable { + private int mParentLocation[] = new int[2]; + + void postShowFloatingWindow() { + mFloatingContainer.measure(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + mFloatingWindow.setWidth(mFloatingContainer.getMeasuredWidth()); + mFloatingWindow.setHeight(mFloatingContainer.getMeasuredHeight()); + post(this); + } + + void cancelShowing() { + if (mFloatingWindow.isShowing()) { + mFloatingWindow.dismiss(); + } + removeCallbacks(this); + } + + public void run() { + mCandidatesContainer.getLocationInWindow(mParentLocation); + + if (!mFloatingWindow.isShowing()) { + mFloatingWindow.showAtLocation(mCandidatesContainer, + Gravity.LEFT | Gravity.TOP, mParentLocation[0], + mParentLocation[1] -mFloatingWindow.getHeight()); + } else { + mFloatingWindow + .update(mParentLocation[0], + mParentLocation[1] - mFloatingWindow.getHeight(), + mFloatingWindow.getWidth(), + mFloatingWindow.getHeight()); + } + } + } + + /** + * Used to notify IME that the user selects a candidate or performs an + * gesture. + */ + public class ChoiceNotifier extends Handler implements + CandidateViewListener { + PinyinIME mIme; + + ChoiceNotifier(PinyinIME ime) { + mIme = ime; + } + + public void onClickChoice(int choiceId) { + if (choiceId >= 0) { + mIme.onChoiceTouched(choiceId); + } + } + + public void onToLeftGesture() { + if (ImeState.STATE_COMPOSING == mImeState) { + changeToStateInput(true); + } + mCandidatesContainer.pageForward(true, false); + } + + public void onToRightGesture() { + if (ImeState.STATE_COMPOSING == mImeState) { + changeToStateInput(true); + } + mCandidatesContainer.pageBackward(true, false); + } + + public void onToTopGesture() { + } + + public void onToBottomGesture() { + } + } + + public class OnGestureListener extends + GestureDetector.SimpleOnGestureListener { + /** + * When user presses and drags, the minimum x-distance to make a + * response to the drag event. + */ + private static final int MIN_X_FOR_DRAG = 60; + + /** + * When user presses and drags, the minimum y-distance to make a + * response to the drag event. + */ + private static final int MIN_Y_FOR_DRAG = 40; + + /** + * Velocity threshold for a screen-move gesture. If the minimum + * x-velocity is less than it, no gesture. + */ + static private final float VELOCITY_THRESHOLD_X1 = 0.3f; + + /** + * Velocity threshold for a screen-move gesture. If the maximum + * x-velocity is less than it, no gesture. + */ + static private final float VELOCITY_THRESHOLD_X2 = 0.7f; + + /** + * Velocity threshold for a screen-move gesture. If the minimum + * y-velocity is less than it, no gesture. + */ + static private final float VELOCITY_THRESHOLD_Y1 = 0.2f; + + /** + * Velocity threshold for a screen-move gesture. If the maximum + * y-velocity is less than it, no gesture. + */ + static private final float VELOCITY_THRESHOLD_Y2 = 0.45f; + + /** If it false, we will not response detected gestures. */ + private boolean mReponseGestures; + + /** The minimum X velocity observed in the gesture. */ + private float mMinVelocityX = Float.MAX_VALUE; + + /** The minimum Y velocity observed in the gesture. */ + private float mMinVelocityY = Float.MAX_VALUE; + + /** The first down time for the series of touch events for an action. */ + private long mTimeDown; + + /** The last time when onScroll() is called. */ + private long mTimeLastOnScroll; + + /** This flag used to indicate that this gesture is not a gesture. */ + private boolean mNotGesture; + + /** This flag used to indicate that this gesture has been recognized. */ + private boolean mGestureRecognized; + + public OnGestureListener(boolean reponseGestures) { + mReponseGestures = reponseGestures; + } + + @Override + public boolean onDown(MotionEvent e) { + mMinVelocityX = Integer.MAX_VALUE; + mMinVelocityY = Integer.MAX_VALUE; + mTimeDown = e.getEventTime(); + mTimeLastOnScroll = mTimeDown; + mNotGesture = false; + mGestureRecognized = false; + return false; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, + float distanceX, float distanceY) { + if (mNotGesture) return false; + if (mGestureRecognized) return true; + + if (Math.abs(e1.getX() - e2.getX()) < MIN_X_FOR_DRAG + && Math.abs(e1.getY() - e2.getY()) < MIN_Y_FOR_DRAG) + return false; + + long timeNow = e2.getEventTime(); + long spanTotal = timeNow - mTimeDown; + long spanThis = timeNow - mTimeLastOnScroll; + if (0 == spanTotal) spanTotal = 1; + if (0 == spanThis) spanThis = 1; + + float vXTotal = (e2.getX() - e1.getX()) / spanTotal; + float vYTotal = (e2.getY() - e1.getY()) / spanTotal; + + // The distances are from the current point to the previous one. + float vXThis = -distanceX / spanThis; + float vYThis = -distanceY / spanThis; + + float kX = vXTotal * vXThis; + float kY = vYTotal * vYThis; + float k1 = kX + kY; + float k2 = Math.abs(kX) + Math.abs(kY); + + if (k1 / k2 < 0.8) { + mNotGesture = true; + return false; + } + float absVXTotal = Math.abs(vXTotal); + float absVYTotal = Math.abs(vYTotal); + if (absVXTotal < mMinVelocityX) { + mMinVelocityX = absVXTotal; + } + if (absVYTotal < mMinVelocityY) { + mMinVelocityY = absVYTotal; + } + + if (mMinVelocityX < VELOCITY_THRESHOLD_X1 + && mMinVelocityY < VELOCITY_THRESHOLD_Y1) { + mNotGesture = true; + return false; + } + + if (vXTotal > VELOCITY_THRESHOLD_X2 + && absVYTotal < VELOCITY_THRESHOLD_Y2) { + if (mReponseGestures) onDirectionGesture(Gravity.RIGHT); + mGestureRecognized = true; + } else if (vXTotal < -VELOCITY_THRESHOLD_X2 + && absVYTotal < VELOCITY_THRESHOLD_Y2) { + if (mReponseGestures) onDirectionGesture(Gravity.LEFT); + mGestureRecognized = true; + } else if (vYTotal > VELOCITY_THRESHOLD_Y2 + && absVXTotal < VELOCITY_THRESHOLD_X2) { + if (mReponseGestures) onDirectionGesture(Gravity.BOTTOM); + mGestureRecognized = true; + } else if (vYTotal < -VELOCITY_THRESHOLD_Y2 + && absVXTotal < VELOCITY_THRESHOLD_X2) { + if (mReponseGestures) onDirectionGesture(Gravity.TOP); + mGestureRecognized = true; + } + + mTimeLastOnScroll = timeNow; + return mGestureRecognized; + } + + @Override + public boolean onFling(MotionEvent me1, MotionEvent me2, + float velocityX, float velocityY) { + return mGestureRecognized; + } + + public void onDirectionGesture(int gravity) { + if (Gravity.NO_GRAVITY == gravity) { + return; + } + + if (Gravity.LEFT == gravity || Gravity.RIGHT == gravity) { + if (mCandidatesContainer.isShown()) { + if (Gravity.LEFT == gravity) { + mCandidatesContainer.pageForward(true, true); + } else { + mCandidatesContainer.pageBackward(true, true); + } + return; + } + } + } + } + + /** + * Connection used for binding to the Pinyin decoding service. + */ + public class PinyinDecoderServiceConnection implements ServiceConnection { + public void onServiceConnected(ComponentName name, IBinder service) { + mDecInfo.mIPinyinDecoderService = IPinyinDecoderService.Stub + .asInterface(service); + } + + public void onServiceDisconnected(ComponentName name) { + } + } + + public enum ImeState { + STATE_BYPASS, STATE_IDLE, STATE_INPUT, STATE_COMPOSING, STATE_PREDICT, + STATE_APP_COMPLETION + } + + public class DecodingInfo { + /** + * Maximum length of the Pinyin string + */ + private static final int PY_STRING_MAX = 28; + + /** + * Maximum number of candidates to display in one page. + */ + private static final int MAX_PAGE_SIZE_DISPLAY = 10; + + /** + * Spelling (Pinyin) string. + */ + private StringBuffer mSurface; + + /** + * Byte buffer used as the Pinyin string parameter for native function + * call. + */ + private byte mPyBuf[]; + + /** + * The length of surface string successfully decoded by engine. + */ + private int mSurfaceDecodedLen; + + /** + * Composing string. + */ + private String mComposingStr; + + /** + * Length of the active composing string. + */ + private int mActiveCmpsLen; + + /** + * Composing string for display, it is copied from mComposingStr, and + * add spaces between spellings. + **/ + private String mComposingStrDisplay; + + /** + * Length of the active composing string for display. + */ + private int mActiveCmpsDisplayLen; + + /** + * The first full sentence choice. + */ + private String mFullSent; + + /** + * Number of characters which have been fixed. + */ + private int mFixedLen; + + /** + * If this flag is true, selection is finished. + */ + private boolean mFinishSelection; + + /** + * The starting position for each spelling. The first one is the number + * of the real starting position elements. + */ + private int mSplStart[]; + + /** + * Editing cursor in mSurface. + */ + private int mCursorPos; + + /** + * Remote Pinyin-to-Hanzi decoding engine service. + */ + private IPinyinDecoderService mIPinyinDecoderService; + + /** + * The complication information suggested by application. + */ + private CompletionInfo[] mAppCompletions; + + /** + * The total number of choices for display. The list may only contains + * the first part. If user tries to navigate to next page which is not + * in the result list, we need to get these items. + **/ + public int mTotalChoicesNum; + + /** + * Candidate list. The first one is the full-sentence candidate. + */ + public List<String> mCandidatesList = new Vector<String>(); + + /** + * Element i stores the starting position of page i. + */ + public Vector<Integer> mPageStart = new Vector<Integer>(); + + /** + * Element i stores the number of characters to page i. + */ + public Vector<Integer> mCnToPage = new Vector<Integer>(); + + /** + * The position to delete in Pinyin string. If it is less than 0, IME + * will do an incremental search, otherwise IME will do a deletion + * operation. if {@link #mIsPosInSpl} is true, IME will delete the whole + * string for mPosDelSpl-th spelling, otherwise it will only delete + * mPosDelSpl-th character in the Pinyin string. + */ + public int mPosDelSpl = -1; + + /** + * If {@link #mPosDelSpl} is big than or equal to 0, this member is used + * to indicate that whether the postion is counted in spelling id or + * character. + */ + public boolean mIsPosInSpl; + + public DecodingInfo() { + mSurface = new StringBuffer(); + mSurfaceDecodedLen = 0; + } + + public void reset() { + mSurface.delete(0, mSurface.length()); + mSurfaceDecodedLen = 0; + mCursorPos = 0; + mFullSent = ""; + mFixedLen = 0; + mFinishSelection = false; + mComposingStr = ""; + mComposingStrDisplay = ""; + mActiveCmpsLen = 0; + mActiveCmpsDisplayLen = 0; + + resetCandidates(); + } + + public boolean isCandidatesListEmpty() { + return mCandidatesList.size() == 0; + } + + public boolean isSplStrFull() { + if (mSurface.length() >= PY_STRING_MAX - 1) return true; + return false; + } + + public void addSplChar(char ch, boolean reset) { + if (reset) { + mSurface.delete(0, mSurface.length()); + mSurfaceDecodedLen = 0; + mCursorPos = 0; + try { + mIPinyinDecoderService.imResetSearch(); + } catch (RemoteException e) { + } + } + mSurface.insert(mCursorPos, ch); + mCursorPos++; + } + + // Prepare to delete before cursor. We may delete a spelling char if + // the cursor is in the range of unfixed part, delete a whole spelling + // if the cursor in inside the range of the fixed part. + // This function only marks the position used to delete. + public void prepareDeleteBeforeCursor() { + if (mCursorPos > 0) { + int pos; + for (pos = 0; pos < mFixedLen; pos++) { + if (mSplStart[pos + 2] >= mCursorPos + && mSplStart[pos + 1] < mCursorPos) { + mPosDelSpl = pos; + mCursorPos = mSplStart[pos + 1]; + mIsPosInSpl = true; + break; + } + } + if (mPosDelSpl < 0) { + mPosDelSpl = mCursorPos - 1; + mCursorPos--; + mIsPosInSpl = false; + } + } + } + + public int length() { + return mSurface.length(); + } + + public char charAt(int index) { + return mSurface.charAt(index); + } + + public StringBuffer getOrigianlSplStr() { + return mSurface; + } + + public int getSplStrDecodedLen() { + return mSurfaceDecodedLen; + } + + public int[] getSplStart() { + return mSplStart; + } + + public String getComposingStr() { + return mComposingStr; + } + + public String getComposingStrActivePart() { + assert (mActiveCmpsLen <= mComposingStr.length()); + return mComposingStr.substring(0, mActiveCmpsLen); + } + + public int getActiveCmpsLen() { + return mActiveCmpsLen; + } + + public String getComposingStrForDisplay() { + return mComposingStrDisplay; + } + + public int getActiveCmpsDisplayLen() { + return mActiveCmpsDisplayLen; + } + + public String getFullSent() { + return mFullSent; + } + + public String getCurrentFullSent(int activeCandPos) { + try { + String retStr = mFullSent.substring(0, mFixedLen); + retStr += mCandidatesList.get(activeCandPos); + return retStr; + } catch (Exception e) { + return ""; + } + } + + public void resetCandidates() { + mCandidatesList.clear(); + mTotalChoicesNum = 0; + + mPageStart.clear(); + mPageStart.add(0); + mCnToPage.clear(); + mCnToPage.add(0); + } + + public boolean candidatesFromApp() { + return ImeState.STATE_APP_COMPLETION == mImeState; + } + + public boolean canDoPrediction() { + return mComposingStr.length() == mFixedLen; + } + + public boolean selectionFinished() { + return mFinishSelection; + } + + // After the user chooses a candidate, input method will do a + // re-decoding and give the new candidate list. + // If candidate id is less than 0, means user is inputting Pinyin, + // not selecting any choice. + private void chooseDecodingCandidate(int candId) { + if (mImeState != ImeState.STATE_PREDICT) { + resetCandidates(); + int totalChoicesNum = 0; + try { + if (candId < 0) { + if (length() == 0) { + totalChoicesNum = 0; + } else { + if (mPyBuf == null) + mPyBuf = new byte[PY_STRING_MAX]; + for (int i = 0; i < length(); i++) + mPyBuf[i] = (byte) charAt(i); + mPyBuf[length()] = 0; + + if (mPosDelSpl < 0) { + totalChoicesNum = mIPinyinDecoderService + .imSearch(mPyBuf, length()); + } else { + boolean clear_fixed_this_step = true; + if (ImeState.STATE_COMPOSING == mImeState) { + clear_fixed_this_step = false; + } + totalChoicesNum = mIPinyinDecoderService + .imDelSearch(mPosDelSpl, mIsPosInSpl, + clear_fixed_this_step); + mPosDelSpl = -1; + } + } + } else { + totalChoicesNum = mIPinyinDecoderService + .imChoose(candId); + } + } catch (RemoteException e) { + } + updateDecInfoForSearch(totalChoicesNum); + } + } + + private void updateDecInfoForSearch(int totalChoicesNum) { + mTotalChoicesNum = totalChoicesNum; + if (mTotalChoicesNum < 0) { + mTotalChoicesNum = 0; + return; + } + + try { + String pyStr; + + mSplStart = mIPinyinDecoderService.imGetSplStart(); + pyStr = mIPinyinDecoderService.imGetPyStr(false); + mSurfaceDecodedLen = mIPinyinDecoderService.imGetPyStrLen(true); + assert (mSurfaceDecodedLen <= pyStr.length()); + + mFullSent = mIPinyinDecoderService.imGetChoice(0); + mFixedLen = mIPinyinDecoderService.imGetFixedLen(); + + // Update the surface string to the one kept by engine. + mSurface.replace(0, mSurface.length(), pyStr); + + if (mCursorPos > mSurface.length()) + mCursorPos = mSurface.length(); + mComposingStr = mFullSent.substring(0, mFixedLen) + + mSurface.substring(mSplStart[mFixedLen + 1]); + + mActiveCmpsLen = mComposingStr.length(); + if (mSurfaceDecodedLen > 0) { + mActiveCmpsLen = mActiveCmpsLen + - (mSurface.length() - mSurfaceDecodedLen); + } + + // Prepare the display string. + if (0 == mSurfaceDecodedLen) { + mComposingStrDisplay = mComposingStr; + mActiveCmpsDisplayLen = mComposingStr.length(); + } else { + mComposingStrDisplay = mFullSent.substring(0, mFixedLen); + for (int pos = mFixedLen + 1; pos < mSplStart.length - 1; pos++) { + mComposingStrDisplay += mSurface.substring( + mSplStart[pos], mSplStart[pos + 1]); + if (mSplStart[pos + 1] < mSurfaceDecodedLen) { + mComposingStrDisplay += " "; + } + } + mActiveCmpsDisplayLen = mComposingStrDisplay.length(); + if (mSurfaceDecodedLen < mSurface.length()) { + mComposingStrDisplay += mSurface + .substring(mSurfaceDecodedLen); + } + } + + if (mSplStart.length == mFixedLen + 2) { + mFinishSelection = true; + } else { + mFinishSelection = false; + } + } catch (RemoteException e) { + Log.w(TAG, "PinyinDecoderService died", e); + } catch (Exception e) { + mTotalChoicesNum = 0; + mComposingStr = ""; + } + // Prepare page 0. + if (!mFinishSelection) { + preparePage(0); + } + } + + private void choosePredictChoice(int choiceId) { + if (ImeState.STATE_PREDICT != mImeState || choiceId < 0 + || choiceId >= mTotalChoicesNum) { + return; + } + + String tmp = mCandidatesList.get(choiceId); + + resetCandidates(); + + mCandidatesList.add(tmp); + mTotalChoicesNum = 1; + + mSurface.replace(0, mSurface.length(), ""); + mCursorPos = 0; + mFullSent = tmp; + mFixedLen = tmp.length(); + mComposingStr = mFullSent; + mActiveCmpsLen = mFixedLen; + + mFinishSelection = true; + } + + public String getCandidate(int candId) { + // Only loaded items can be gotten, so we use mCandidatesList.size() + // instead mTotalChoiceNum. + if (candId < 0 || candId > mCandidatesList.size()) { + return null; + } + return mCandidatesList.get(candId); + } + + private void getCandiagtesForCache() { + int fetchStart = mCandidatesList.size(); + int fetchSize = mTotalChoicesNum - fetchStart; + if (fetchSize > MAX_PAGE_SIZE_DISPLAY) { + fetchSize = MAX_PAGE_SIZE_DISPLAY; + } + try { + List<String> newList = null; + if (ImeState.STATE_INPUT == mImeState || + ImeState.STATE_IDLE == mImeState || + ImeState.STATE_COMPOSING == mImeState){ + newList = mIPinyinDecoderService.imGetChoiceList( + fetchStart, fetchSize, mFixedLen); + } else if (ImeState.STATE_PREDICT == mImeState) { + newList = mIPinyinDecoderService.imGetPredictList( + fetchStart, fetchSize); + } else if (ImeState.STATE_APP_COMPLETION == mImeState) { + newList = new ArrayList<String>(); + if (null != mAppCompletions) { + for (int pos = fetchStart; pos < fetchSize; pos++) { + CompletionInfo ci = mAppCompletions[pos]; + if (null != ci) { + CharSequence s = ci.getText(); + if (null != s) newList.add(s.toString()); + } + } + } + } + mCandidatesList.addAll(newList); + } catch (RemoteException e) { + Log.w(TAG, "PinyinDecoderService died", e); + } + } + + public boolean pageReady(int pageNo) { + // If the page number is less than 0, return false + if (pageNo < 0) return false; + + // Page pageNo's ending information is not ready. + if (mPageStart.size() <= pageNo + 1) { + return false; + } + + return true; + } + + public boolean preparePage(int pageNo) { + // If the page number is less than 0, return false + if (pageNo < 0) return false; + + // Make sure the starting information for page pageNo is ready. + if (mPageStart.size() <= pageNo) { + return false; + } + + // Page pageNo's ending information is also ready. + if (mPageStart.size() > pageNo + 1) { + return true; + } + + // If cached items is enough for page pageNo. + if (mCandidatesList.size() - mPageStart.elementAt(pageNo) >= MAX_PAGE_SIZE_DISPLAY) { + return true; + } + + // Try to get more items from engine + getCandiagtesForCache(); + + // Try to find if there are available new items to display. + // If no new item, return false; + if (mPageStart.elementAt(pageNo) >= mCandidatesList.size()) { + return false; + } + + // If there are new items, return true; + return true; + } + + public void preparePredicts(CharSequence history) { + if (null == history) return; + + resetCandidates(); + + if (Settings.getPrediction()) { + String preEdit = history.toString(); + int predictNum = 0; + if (null != preEdit) { + try { + mTotalChoicesNum = mIPinyinDecoderService + .imGetPredictsNum(preEdit); + } catch (RemoteException e) { + return; + } + } + } + + preparePage(0); + mFinishSelection = false; + } + + private void prepareAppCompletions(CompletionInfo completions[]) { + resetCandidates(); + mAppCompletions = completions; + mTotalChoicesNum = completions.length; + preparePage(0); + mFinishSelection = false; + return; + } + + public int getCurrentPageSize(int currentPage) { + if (mPageStart.size() <= currentPage + 1) return 0; + return mPageStart.elementAt(currentPage + 1) + - mPageStart.elementAt(currentPage); + } + + public int getCurrentPageStart(int currentPage) { + if (mPageStart.size() < currentPage + 1) return mTotalChoicesNum; + return mPageStart.elementAt(currentPage); + } + + public boolean pageForwardable(int currentPage) { + if (mPageStart.size() <= currentPage + 1) return false; + if (mPageStart.elementAt(currentPage + 1) >= mTotalChoicesNum) { + return false; + } + return true; + } + + public boolean pageBackwardable(int currentPage) { + if (currentPage > 0) return true; + return false; + } + + public boolean charBeforeCursorIsSeparator() { + int len = mSurface.length(); + if (mCursorPos > len) return false; + if (mCursorPos > 0 && mSurface.charAt(mCursorPos - 1) == '\'') { + return true; + } + return false; + } + + public int getCursorPos() { + return mCursorPos; + } + + public int getCursorPosInCmps() { + int cursorPos = mCursorPos; + int fixedLen = 0; + + for (int hzPos = 0; hzPos < mFixedLen; hzPos++) { + if (mCursorPos >= mSplStart[hzPos + 2]) { + cursorPos -= mSplStart[hzPos + 2] - mSplStart[hzPos + 1]; + cursorPos += 1; + } + } + return cursorPos; + } + + public int getCursorPosInCmpsDisplay() { + int cursorPos = getCursorPosInCmps(); + // +2 is because: one for mSplStart[0], which is used for other + // purpose(The length of the segmentation string), and another + // for the first spelling which does not need a space before it. + for (int pos = mFixedLen + 2; pos < mSplStart.length - 1; pos++) { + if (mCursorPos <= mSplStart[pos]) { + break; + } else { + cursorPos++; + } + } + return cursorPos; + } + + public void moveCursorToEdge(boolean left) { + if (left) + mCursorPos = 0; + else + mCursorPos = mSurface.length(); + } + + // Move cursor. If offset is 0, this function can be used to adjust + // the cursor into the bounds of the string. + public void moveCursor(int offset) { + if (offset > 1 || offset < -1) return; + + if (offset != 0) { + int hzPos = 0; + for (hzPos = 0; hzPos <= mFixedLen; hzPos++) { + if (mCursorPos == mSplStart[hzPos + 1]) { + if (offset < 0) { + if (hzPos > 0) { + offset = mSplStart[hzPos] + - mSplStart[hzPos + 1]; + } + } else { + if (hzPos < mFixedLen) { + offset = mSplStart[hzPos + 2] + - mSplStart[hzPos + 1]; + } + } + break; + } + } + } + mCursorPos += offset; + if (mCursorPos < 0) { + mCursorPos = 0; + } else if (mCursorPos > mSurface.length()) { + mCursorPos = mSurface.length(); + } + } + + public int getSplNum() { + return mSplStart[0]; + } + + public int getFixedLen() { + return mFixedLen; + } + } +} diff --git a/src/com/android/inputmethod/pinyin/Settings.java b/src/com/android/inputmethod/pinyin/Settings.java new file mode 100644 index 0000000..c05f605 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/Settings.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; + +/** + * Class used to maintain settings. + */ +public class Settings { + private static final String ANDPY_CONFS_KEYSOUND_KEY = "Sound"; + private static final String ANDPY_CONFS_VIBRATE_KEY = "Vibrate"; + private static final String ANDPY_CONFS_PREDICTION_KEY = "Prediction"; + + private static boolean mKeySound; + private static boolean mVibrate; + private static boolean mPrediction; + + private static Settings mInstance = null; + + private static int mRefCount = 0; + + private static SharedPreferences mSharedPref = null; + + protected Settings(SharedPreferences pref) { + mSharedPref = pref; + initConfs(); + } + + public static Settings getInstance(SharedPreferences pref) { + if (mInstance == null) { + mInstance = new Settings(pref); + } + assert (pref == mSharedPref); + mRefCount++; + return mInstance; + } + + public static void writeBack() { + Editor editor = mSharedPref.edit(); + editor.putBoolean(ANDPY_CONFS_VIBRATE_KEY, mVibrate); + editor.putBoolean(ANDPY_CONFS_KEYSOUND_KEY, mKeySound); + editor.putBoolean(ANDPY_CONFS_PREDICTION_KEY, mPrediction); + editor.commit(); + } + + public static void releaseInstance() { + mRefCount--; + if (mRefCount == 0) { + mInstance = null; + } + } + + private void initConfs() { + mKeySound = mSharedPref.getBoolean(ANDPY_CONFS_KEYSOUND_KEY, true); + mVibrate = mSharedPref.getBoolean(ANDPY_CONFS_VIBRATE_KEY, false); + mPrediction = mSharedPref.getBoolean(ANDPY_CONFS_PREDICTION_KEY, true); + } + + public static boolean getKeySound() { + return mKeySound; + } + + public static void setKeySound(boolean v) { + if (mKeySound == v) return; + mKeySound = v; + } + + public static boolean getVibrate() { + return mVibrate; + } + + public static void setVibrate(boolean v) { + if (mVibrate == v) return; + mVibrate = v; + } + + public static boolean getPrediction() { + return mPrediction; + } + + public static void setPrediction(boolean v) { + if (mPrediction == v) return; + mPrediction = v; + } +} diff --git a/src/com/android/inputmethod/pinyin/SettingsActivity.java b/src/com/android/inputmethod/pinyin/SettingsActivity.java new file mode 100644 index 0000000..7d23d8e --- /dev/null +++ b/src/com/android/inputmethod/pinyin/SettingsActivity.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import java.util.List; + +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceGroup; +import android.preference.PreferenceManager; +import android.preference.PreferenceScreen; +import com.android.inputmethod.pinyin.Settings; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; + +/** + * Setting activity of Pinyin IME. + */ +public class SettingsActivity extends PreferenceActivity implements + Preference.OnPreferenceChangeListener { + + private static String TAG = "SettingsActivity"; + + private CheckBoxPreference mKeySoundPref; + private CheckBoxPreference mVibratePref; + private CheckBoxPreference mPredictionPref; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.settings); + + PreferenceScreen prefSet = getPreferenceScreen(); + + mKeySoundPref = (CheckBoxPreference) prefSet + .findPreference(getString(R.string.setting_sound_key)); + mVibratePref = (CheckBoxPreference) prefSet + .findPreference(getString(R.string.setting_vibrate_key)); + mPredictionPref = (CheckBoxPreference) prefSet + .findPreference(getString(R.string.setting_prediction_key)); + + prefSet.setOnPreferenceChangeListener(this); + + Settings.getInstance(PreferenceManager + .getDefaultSharedPreferences(getApplicationContext())); + + updatePreference(prefSet, getString(R.string.setting_advanced_key)); + + updateWidgets(); + } + + @Override + protected void onResume() { + super.onResume(); + updateWidgets(); + } + + @Override + protected void onDestroy() { + Settings.releaseInstance(); + super.onDestroy(); + } + + @Override + protected void onPause() { + super.onPause(); + Settings.setKeySound(mKeySoundPref.isChecked()); + Settings.setVibrate(mVibratePref.isChecked()); + Settings.setPrediction(mPredictionPref.isChecked()); + + Settings.writeBack(); + } + + public boolean onPreferenceChange(Preference preference, Object newValue) { + return true; + } + + private void updateWidgets() { + mKeySoundPref.setChecked(Settings.getKeySound()); + mVibratePref.setChecked(Settings.getVibrate()); + mPredictionPref.setChecked(Settings.getPrediction()); + } + + public void updatePreference(PreferenceGroup parentPref, String prefKey) { + Preference preference = parentPref.findPreference(prefKey); + if (preference == null) { + return; + } + Intent intent = preference.getIntent(); + if (intent != null) { + PackageManager pm = getPackageManager(); + List<ResolveInfo> list = pm.queryIntentActivities(intent, 0); + int listSize = list.size(); + if (listSize == 0) + parentPref.removePreference(preference); + } + } +} diff --git a/src/com/android/inputmethod/pinyin/SkbContainer.java b/src/com/android/inputmethod/pinyin/SkbContainer.java new file mode 100644 index 0000000..2294860 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/SkbContainer.java @@ -0,0 +1,642 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import android.content.Context; +import android.content.res.Resources; +import android.inputmethodservice.InputMethodService; +import android.os.Handler; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.PopupWindow; +import android.widget.RelativeLayout; +import android.widget.ViewFlipper; + +/** + * The top container to host soft keyboard view(s). + */ +public class SkbContainer extends RelativeLayout implements OnTouchListener { + /** + * For finger touch, user tends to press the bottom part of the target key, + * or he/she even presses the area out of it, so it is necessary to make a + * simple bias correction. If the input method runs on emulator, no bias + * correction will be used. + */ + private static final int Y_BIAS_CORRECTION = -10; + + /** + * Used to skip these move events whose position is too close to the + * previous touch events. + */ + private static final int MOVE_TOLERANCE = 6; + + /** + * If this member is true, PopupWindow is used to show on-key highlight + * effect. + */ + private static boolean POPUPWINDOW_FOR_PRESSED_UI = false; + + /** + * The current soft keyboard layout. + * + * @see com.android.inputmethod.pinyin.InputModeSwitcher for detailed layout + * definitions. + */ + private int mSkbLayout = 0; + + /** + * The input method service. + */ + private InputMethodService mService; + + /** + * Input mode switcher used to switch between different modes like Chinese, + * English, etc. + */ + private InputModeSwitcher mInputModeSwitcher; + + /** + * The gesture detector. + */ + private GestureDetector mGestureDetector; + + private Environment mEnvironment; + + private ViewFlipper mSkbFlipper; + + /** + * The popup balloon hint for key press/release. + */ + private BalloonHint mBalloonPopup; + + /** + * The on-key balloon hint for key press/release. + */ + private BalloonHint mBalloonOnKey = null; + + /** The major sub soft keyboard. */ + private SoftKeyboardView mMajorView; + + /** + * The last parameter when function {@link #toggleCandidateMode(boolean)} + * was called. + */ + private boolean mLastCandidatesShowing; + + /** Used to indicate whether a popup soft keyboard is shown. */ + private boolean mPopupSkbShow = false; + + /** + * Used to indicate whether a popup soft keyboard is just shown, and waits + * for the touch event to release. After the release, the popup window can + * response to touch events. + **/ + private boolean mPopupSkbNoResponse = false; + + /** Popup sub keyboard. */ + private PopupWindow mPopupSkb; + + /** The view of the popup sub soft keyboard. */ + private SoftKeyboardView mPopupSkbView; + + private int mPopupX; + + private int mPopupY; + + /** + * When user presses a key, a timer is started, when it times out, it is + * necessary to detect whether user still holds the key. + */ + private volatile boolean mWaitForTouchUp = false; + + /** + * When user drags on the soft keyboard and the distance is enough, this + * drag will be recognized as a gesture and a gesture-based action will be + * taken, in this situation, ignore the consequent events. + */ + private volatile boolean mDiscardEvent = false; + + /** + * For finger touch, user tends to press the bottom part of the target key, + * or he/she even presses the area out of it, so it is necessary to make a + * simple bias correction in Y. + */ + private int mYBiasCorrection = 0; + + /** + * The x coordination of the last touch event. + */ + private int mXLast; + + /** + * The y coordination of the last touch event. + */ + private int mYLast; + + /** + * The soft keyboard view. + */ + private SoftKeyboardView mSkv; + + /** + * The position of the soft keyboard view in the container. + */ + private int mSkvPosInContainer[] = new int[2]; + + /** + * The key pressed by user. + */ + private SoftKey mSoftKeyDown = null; + + /** + * Used to timeout a press if user holds the key for a long time. + */ + private LongPressTimer mLongPressTimer; + + /** + * For temporary use. + */ + private int mXyPosTmp[] = new int[2]; + + public SkbContainer(Context context, AttributeSet attrs) { + super(context, attrs); + + mEnvironment = Environment.getInstance(); + + mLongPressTimer = new LongPressTimer(this); + + // If it runs on an emulator, no bias correction + if ("1".equals(SystemProperties.get("ro.kernel.qemu"))) { + mYBiasCorrection = 0; + } else { + mYBiasCorrection = Y_BIAS_CORRECTION; + } + mBalloonPopup = new BalloonHint(context, this, MeasureSpec.AT_MOST); + if (POPUPWINDOW_FOR_PRESSED_UI) { + mBalloonOnKey = new BalloonHint(context, this, MeasureSpec.AT_MOST); + } + + mPopupSkb = new PopupWindow(mContext); + mPopupSkb.setBackgroundDrawable(null); + mPopupSkb.setClippingEnabled(false); + } + + public void setService(InputMethodService service) { + mService = service; + } + + public void setInputModeSwitcher(InputModeSwitcher inputModeSwitcher) { + mInputModeSwitcher = inputModeSwitcher; + } + + public void setGestureDetector(GestureDetector gestureDetector) { + mGestureDetector = gestureDetector; + } + + public boolean isCurrentSkbSticky() { + if (null == mMajorView) return true; + SoftKeyboard skb = mMajorView.getSoftKeyboard(); + if (null != skb) { + return skb.getStickyFlag(); + } + return true; + } + + public void toggleCandidateMode(boolean candidatesShowing) { + if (null == mMajorView || !mInputModeSwitcher.isChineseText() + || mLastCandidatesShowing == candidatesShowing) return; + mLastCandidatesShowing = candidatesShowing; + + SoftKeyboard skb = mMajorView.getSoftKeyboard(); + if (null == skb) return; + + int state = mInputModeSwitcher.getTooggleStateForCnCand(); + if (!candidatesShowing) { + skb.disableToggleState(state, false); + skb.enableToggleStates(mInputModeSwitcher.getToggleStates()); + } else { + skb.enableToggleState(state, false); + } + + mMajorView.invalidate(); + } + + public void updateInputMode() { + int skbLayout = mInputModeSwitcher.getSkbLayout(); + if (mSkbLayout != skbLayout) { + mSkbLayout = skbLayout; + updateSkbLayout(); + } + + mLastCandidatesShowing = false; + + if (null == mMajorView) return; + + SoftKeyboard skb = mMajorView.getSoftKeyboard(); + if (null == skb) return; + skb.enableToggleStates(mInputModeSwitcher.getToggleStates()); + invalidate(); + return; + } + + private void updateSkbLayout() { + int screenWidth = mEnvironment.getScreenWidth(); + int keyHeight = mEnvironment.getKeyHeight(); + int skbHeight = mEnvironment.getSkbHeight(); + + Resources r = mContext.getResources(); + if (null == mSkbFlipper) { + mSkbFlipper = (ViewFlipper) findViewById(R.id.alpha_floatable); + } + mMajorView = (SoftKeyboardView) mSkbFlipper.getChildAt(0); + + SoftKeyboard majorSkb = null; + SkbPool skbPool = SkbPool.getInstance(); + + switch (mSkbLayout) { + case R.xml.skb_qwerty: + majorSkb = skbPool.getSoftKeyboard(R.xml.skb_qwerty, + R.xml.skb_qwerty, screenWidth, skbHeight, mContext); + break; + + case R.xml.skb_sym1: + majorSkb = skbPool.getSoftKeyboard(R.xml.skb_sym1, R.xml.skb_sym1, + screenWidth, skbHeight, mContext); + break; + + case R.xml.skb_sym2: + majorSkb = skbPool.getSoftKeyboard(R.xml.skb_sym2, R.xml.skb_sym2, + screenWidth, skbHeight, mContext); + break; + + case R.xml.skb_smiley: + majorSkb = skbPool.getSoftKeyboard(R.xml.skb_smiley, + R.xml.skb_smiley, screenWidth, skbHeight, mContext); + break; + + case R.xml.skb_phone: + majorSkb = skbPool.getSoftKeyboard(R.xml.skb_phone, + R.xml.skb_phone, screenWidth, skbHeight, mContext); + break; + default: + } + + if (null == majorSkb || !mMajorView.setSoftKeyboard(majorSkb)) { + return; + } + mMajorView.setBalloonHint(mBalloonOnKey, mBalloonPopup, false); + mMajorView.invalidate(); + } + + private void responseKeyEvent(SoftKey sKey) { + if (null == sKey) return; + ((PinyinIME) mService).responseSoftKeyEvent(sKey); + return; + } + + private SoftKeyboardView inKeyboardView(int x, int y, + int positionInParent[]) { + if (mPopupSkbShow) { + if (mPopupX <= x && mPopupX + mPopupSkb.getWidth() > x + && mPopupY <= y && mPopupY + mPopupSkb.getHeight() > y) { + positionInParent[0] = mPopupX; + positionInParent[1] = mPopupY; + mPopupSkbView.setOffsetToSkbContainer(positionInParent); + return mPopupSkbView; + } + return null; + } + + return mMajorView; + } + + private void popupSymbols() { + int popupResId = mSoftKeyDown.getPopupResId(); + if (popupResId > 0) { + int skbContainerWidth = getWidth(); + int skbContainerHeight = getHeight(); + // The paddings of the background are not included. + int miniSkbWidth = (int) (skbContainerWidth * 0.8); + int miniSkbHeight = (int) (skbContainerHeight * 0.23); + + SkbPool skbPool = SkbPool.getInstance(); + SoftKeyboard skb = skbPool.getSoftKeyboard(popupResId, popupResId, + miniSkbWidth, miniSkbHeight, mContext); + if (null == skb) return; + + mPopupX = (skbContainerWidth - skb.getSkbTotalWidth()) / 2; + mPopupY = (skbContainerHeight - skb.getSkbTotalHeight()) / 2; + + if (null == mPopupSkbView) { + mPopupSkbView = new SoftKeyboardView(mContext, null); + mPopupSkbView.onMeasure(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + } + mPopupSkbView.setOnTouchListener(this); + mPopupSkbView.setSoftKeyboard(skb); + mPopupSkbView.setBalloonHint(mBalloonOnKey, mBalloonPopup, true); + + mPopupSkb.setContentView(mPopupSkbView); + mPopupSkb.setWidth(skb.getSkbCoreWidth() + + mPopupSkbView.getPaddingLeft() + + mPopupSkbView.getPaddingRight()); + mPopupSkb.setHeight(skb.getSkbCoreHeight() + + mPopupSkbView.getPaddingTop() + + mPopupSkbView.getPaddingBottom()); + + getLocationInWindow(mXyPosTmp); + mPopupSkb.showAtLocation(this, Gravity.NO_GRAVITY, mPopupX, mPopupY + + mXyPosTmp[1]); + mPopupSkbShow = true; + mPopupSkbNoResponse = true; + // Invalidate itself to dim the current soft keyboards. + dimSoftKeyboard(true); + resetKeyPress(0); + } + } + + private void dimSoftKeyboard(boolean dimSkb) { + mMajorView.dimSoftKeyboard(dimSkb); + } + + private void dismissPopupSkb() { + mPopupSkb.dismiss(); + mPopupSkbShow = false; + dimSoftKeyboard(false); + resetKeyPress(0); + } + + private void resetKeyPress(long delay) { + mLongPressTimer.removeTimer(); + + if (null != mSkv) { + mSkv.resetKeyPress(delay); + } + } + + public boolean handleBack(boolean realAction) { + if (mPopupSkbShow) { + if (!realAction) return true; + + dismissPopupSkb(); + mDiscardEvent = true; + return true; + } + return false; + } + + public void dismissPopups() { + handleBack(true); + resetKeyPress(0); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + Environment env = Environment.getInstance(); + int measuredWidth = env.getScreenWidth(); + int measuredHeight = getPaddingTop(); + measuredHeight += env.getSkbHeight(); + widthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, + MeasureSpec.EXACTLY); + heightMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight, + MeasureSpec.EXACTLY); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + + if (mSkbFlipper.isFlipping()) { + resetKeyPress(0); + return true; + } + + int x = (int) event.getX(); + int y = (int) event.getY(); + // Bias correction + y = y + mYBiasCorrection; + + // Ignore short-distance movement event to get better performance. + if (event.getAction() == MotionEvent.ACTION_MOVE) { + if (Math.abs(x - mXLast) <= MOVE_TOLERANCE + && Math.abs(y - mYLast) <= MOVE_TOLERANCE) { + return true; + } + } + + mXLast = x; + mYLast = y; + + if (!mPopupSkbShow) { + if (mGestureDetector.onTouchEvent(event)) { + resetKeyPress(0); + mDiscardEvent = true; + return true; + } + } + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + resetKeyPress(0); + + mWaitForTouchUp = true; + mDiscardEvent = false; + + mSkv = null; + mSoftKeyDown = null; + mSkv = inKeyboardView(x, y, mSkvPosInContainer); + if (null != mSkv) { + mSoftKeyDown = mSkv.onKeyPress(x - mSkvPosInContainer[0], y + - mSkvPosInContainer[1], mLongPressTimer, false); + } + break; + + case MotionEvent.ACTION_MOVE: + if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) { + break; + } + if (mDiscardEvent) { + resetKeyPress(0); + break; + } + + if (mPopupSkbShow && mPopupSkbNoResponse) { + break; + } + + SoftKeyboardView skv = inKeyboardView(x, y, mSkvPosInContainer); + if (null != skv) { + if (skv != mSkv) { + mSkv = skv; + mSoftKeyDown = mSkv.onKeyPress(x - mSkvPosInContainer[0], y + - mSkvPosInContainer[1], mLongPressTimer, true); + } else if (null != skv) { + if (null != mSkv) { + mSoftKeyDown = mSkv.onKeyMove( + x - mSkvPosInContainer[0], y + - mSkvPosInContainer[1]); + if (null == mSoftKeyDown) { + mDiscardEvent = true; + } + } + } + } + break; + + case MotionEvent.ACTION_UP: + if (mDiscardEvent) { + resetKeyPress(0); + break; + } + + mWaitForTouchUp = false; + + // The view which got the {@link MotionEvent#ACTION_DOWN} event is + // always used to handle this event. + if (null != mSkv) { + mSkv.onKeyRelease(x - mSkvPosInContainer[0], y + - mSkvPosInContainer[1]); + } + + if (!mPopupSkbShow || !mPopupSkbNoResponse) { + responseKeyEvent(mSoftKeyDown); + } + + if (mSkv == mPopupSkbView && !mPopupSkbNoResponse) { + dismissPopupSkb(); + } + mPopupSkbNoResponse = false; + break; + + case MotionEvent.ACTION_CANCEL: + break; + } + + if (null == mSkv) { + return false; + } + + return true; + } + + // Function for interface OnTouchListener, it is used to handle touch events + // which will be delivered to the popup soft keyboard view. + public boolean onTouch(View v, MotionEvent event) { + // Translate the event to fit to the container. + MotionEvent newEv = MotionEvent.obtain(event.getDownTime(), event + .getEventTime(), event.getAction(), event.getX() + mPopupX, + event.getY() + mPopupY, event.getPressure(), event.getSize(), + event.getMetaState(), event.getXPrecision(), event + .getYPrecision(), event.getDeviceId(), event + .getEdgeFlags()); + boolean ret = onTouchEvent(newEv); + return ret; + } + + class LongPressTimer extends Handler implements Runnable { + /** + * When user presses a key for a long time, the timeout interval to + * generate first {@link #LONG_PRESS_KEYNUM1} key events. + */ + public static final int LONG_PRESS_TIMEOUT1 = 500; + + /** + * When user presses a key for a long time, after the first + * {@link #LONG_PRESS_KEYNUM1} key events, this timeout interval will be + * used. + */ + private static final int LONG_PRESS_TIMEOUT2 = 100; + + /** + * When user presses a key for a long time, after the first + * {@link #LONG_PRESS_KEYNUM2} key events, this timeout interval will be + * used. + */ + private static final int LONG_PRESS_TIMEOUT3 = 100; + + /** + * When user presses a key for a long time, after the first + * {@link #LONG_PRESS_KEYNUM1} key events, timeout interval + * {@link #LONG_PRESS_TIMEOUT2} will be used instead. + */ + public static final int LONG_PRESS_KEYNUM1 = 1; + + /** + * When user presses a key for a long time, after the first + * {@link #LONG_PRESS_KEYNUM2} key events, timeout interval + * {@link #LONG_PRESS_TIMEOUT3} will be used instead. + */ + public static final int LONG_PRESS_KEYNUM2 = 3; + + SkbContainer mSkbContainer; + + private int mResponseTimes = 0; + + public LongPressTimer(SkbContainer skbContainer) { + mSkbContainer = skbContainer; + } + + public void startTimer() { + postAtTime(this, SystemClock.uptimeMillis() + LONG_PRESS_TIMEOUT1); + mResponseTimes = 0; + } + + public boolean removeTimer() { + removeCallbacks(this); + return true; + } + + public void run() { + if (mWaitForTouchUp) { + mResponseTimes++; + if (mSoftKeyDown.repeatable()) { + if (mSoftKeyDown.isUserDefKey()) { + if (1 == mResponseTimes) { + if (mInputModeSwitcher + .tryHandleLongPressSwitch(mSoftKeyDown.mKeyCode)) { + mDiscardEvent = true; + resetKeyPress(0); + } + } + } else { + responseKeyEvent(mSoftKeyDown); + long timeout; + if (mResponseTimes < LONG_PRESS_KEYNUM1) { + timeout = LONG_PRESS_TIMEOUT1; + } else if (mResponseTimes < LONG_PRESS_KEYNUM2) { + timeout = LONG_PRESS_TIMEOUT2; + } else { + timeout = LONG_PRESS_TIMEOUT3; + } + postAtTime(this, SystemClock.uptimeMillis() + timeout); + } + } else { + if (1 == mResponseTimes) { + popupSymbols(); + } + } + } + } + } +} diff --git a/src/com/android/inputmethod/pinyin/SkbPool.java b/src/com/android/inputmethod/pinyin/SkbPool.java new file mode 100644 index 0000000..4c46951 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/SkbPool.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import java.util.Vector; + +import android.content.Context; + +/** + * Class used to cache previously loaded soft keyboard layouts. + */ +public class SkbPool { + private static SkbPool mInstance = null; + + private Vector<SkbTemplate> mSkbTemplates = new Vector<SkbTemplate>(); + private Vector<SoftKeyboard> mSoftKeyboards = new Vector<SoftKeyboard>(); + + private SkbPool() { + } + + public static SkbPool getInstance() { + if (null == mInstance) mInstance = new SkbPool(); + return mInstance; + } + + public void resetCachedSkb() { + mSoftKeyboards.clear(); + } + + public SkbTemplate getSkbTemplate(int skbTemplateId, Context context) { + for (int i = 0; i < mSkbTemplates.size(); i++) { + SkbTemplate t = mSkbTemplates.elementAt(i); + if (t.getSkbTemplateId() == skbTemplateId) { + return t; + } + } + + if (null != context) { + XmlKeyboardLoader xkbl = new XmlKeyboardLoader(context); + SkbTemplate t = xkbl.loadSkbTemplate(skbTemplateId); + if (null != t) { + mSkbTemplates.add(t); + return t; + } + } + return null; + } + + // Try to find the keyboard in the pool with the cache id. If there is no + // keyboard found, try to load it with the given xml id. + public SoftKeyboard getSoftKeyboard(int skbCacheId, int skbXmlId, + int skbWidth, int skbHeight, Context context) { + for (int i = 0; i < mSoftKeyboards.size(); i++) { + SoftKeyboard skb = mSoftKeyboards.elementAt(i); + if (skb.getCacheId() == skbCacheId && skb.getSkbXmlId() == skbXmlId) { + skb.setSkbCoreSize(skbWidth, skbHeight); + skb.setNewlyLoadedFlag(false); + return skb; + } + } + if (null != context) { + XmlKeyboardLoader xkbl = new XmlKeyboardLoader(context); + SoftKeyboard skb = xkbl.loadKeyboard(skbXmlId, skbWidth, skbHeight); + if (skb != null) { + if (skb.getCacheFlag()) { + skb.setCacheId(skbCacheId); + mSoftKeyboards.add(skb); + } + } + return skb; + } + return null; + } +} diff --git a/src/com/android/inputmethod/pinyin/SkbTemplate.java b/src/com/android/inputmethod/pinyin/SkbTemplate.java new file mode 100644 index 0000000..9ab53ff --- /dev/null +++ b/src/com/android/inputmethod/pinyin/SkbTemplate.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import android.graphics.drawable.Drawable; + +import java.util.Vector; + +/** + * Key icon definition. It is defined in soft keyboard template. A soft keyboard + * can refer to such an icon in its xml file directly to improve performance. + */ +class KeyIconRecord { + int keyCode; + Drawable icon; + Drawable iconPopup; +} + + +/** + * Default definition for a certain key. It is defined in soft keyboard + * template. A soft keyboard can refer to a default key in its xml file. Nothing + * of the key can be overwritten, including the size. + */ +class KeyRecord { + int keyId; + SoftKey softKey; +} + + +/** + * Soft keyboard template used by soft keyboards to share common resources. In + * this way, memory cost is reduced. + */ +public class SkbTemplate { + private int mSkbTemplateId; + private Drawable mSkbBg; + private Drawable mBalloonBg; + private Drawable mPopupBg; + private float mXMargin = 0; + private float mYMargin = 0; + /** Key type list. */ + private Vector<SoftKeyType> mKeyTypeList = new Vector<SoftKeyType>(); + + /** + * Default key icon list. It is only for keys which do not have popup icons. + */ + private Vector<KeyIconRecord> mKeyIconRecords = new Vector<KeyIconRecord>(); + + /** + * Default key list. + */ + private Vector<KeyRecord> mKeyRecords = new Vector<KeyRecord>(); + + public SkbTemplate(int skbTemplateId) { + mSkbTemplateId = skbTemplateId; + } + + public int getSkbTemplateId() { + return mSkbTemplateId; + } + + public void setBackgrounds(Drawable skbBg, Drawable balloonBg, + Drawable popupBg) { + mSkbBg = skbBg; + mBalloonBg = balloonBg; + mPopupBg = popupBg; + } + + public Drawable getSkbBackground() { + return mSkbBg; + } + + public Drawable getBalloonBackground() { + return mBalloonBg; + } + + public Drawable getPopupBackground() { + return mPopupBg; + } + + public void setMargins(float xMargin, float yMargin) { + mXMargin = xMargin; + mYMargin = yMargin; + } + + public float getXMargin() { + return mXMargin; + } + + public float getYMargin() { + return mYMargin; + } + + public SoftKeyType createKeyType(int id, Drawable bg, Drawable hlBg) { + return new SoftKeyType(id, bg, hlBg); + } + + public boolean addKeyType(SoftKeyType keyType) { + // The newly added item should have the right id. + if (mKeyTypeList.size() != keyType.mKeyTypeId) return false; + mKeyTypeList.add(keyType); + return true; + } + + public SoftKeyType getKeyType(int typeId) { + if (typeId < 0 || typeId > mKeyTypeList.size()) return null; + return mKeyTypeList.elementAt(typeId); + } + + public void addDefaultKeyIcons(int keyCode, Drawable icon, + Drawable iconPopup) { + if (null == icon || null == iconPopup) return; + + KeyIconRecord iconRecord = new KeyIconRecord(); + iconRecord.icon = icon; + iconRecord.iconPopup = iconPopup; + iconRecord.keyCode = keyCode; + + int size = mKeyIconRecords.size(); + int pos = 0; + while (pos < size) { + if (mKeyIconRecords.get(pos).keyCode >= keyCode) break; + pos++; + } + mKeyIconRecords.add(pos, iconRecord); + } + + public Drawable getDefaultKeyIcon(int keyCode) { + int size = mKeyIconRecords.size(); + int pos = 0; + while (pos < size) { + KeyIconRecord iconRecord = mKeyIconRecords.get(pos); + if (iconRecord.keyCode < keyCode) { + pos++; + continue; + } + if (iconRecord.keyCode == keyCode) { + return iconRecord.icon; + } + return null; + } + return null; + } + + public Drawable getDefaultKeyIconPopup(int keyCode) { + int size = mKeyIconRecords.size(); + int pos = 0; + while (pos < size) { + KeyIconRecord iconRecord = mKeyIconRecords.get(pos); + if (iconRecord.keyCode < keyCode) { + pos++; + continue; + } + if (iconRecord.keyCode == keyCode) { + return iconRecord.iconPopup; + } + return null; + } + return null; + } + + public void addDefaultKey(int keyId, SoftKey softKey) { + if (null == softKey) return; + + KeyRecord keyRecord = new KeyRecord(); + keyRecord.keyId = keyId; + keyRecord.softKey = softKey; + + int size = mKeyRecords.size(); + int pos = 0; + while (pos < size) { + if (mKeyRecords.get(pos).keyId >= keyId) break; + pos++; + } + mKeyRecords.add(pos, keyRecord); + } + + public SoftKey getDefaultKey(int keyId) { + int size = mKeyRecords.size(); + int pos = 0; + while (pos < size) { + KeyRecord keyRecord = mKeyRecords.get(pos); + if (keyRecord.keyId < keyId) { + pos++; + continue; + } + if (keyRecord.keyId == keyId) { + return keyRecord.softKey; + } + return null; + } + return null; + } +} + + +class SoftKeyType { + public static final int KEYTYPE_ID_NORMAL_KEY = 0; + + public int mKeyTypeId; + public Drawable mKeyBg; + public Drawable mKeyHlBg; + public int mColor; + public int mColorHl; + public int mColorBalloon; + + SoftKeyType(int id, Drawable bg, Drawable hlBg) { + mKeyTypeId = id; + mKeyBg = bg; + mKeyHlBg = hlBg; + } + + public void setColors(int color, int colorHl, int colorBalloon) { + mColor = color; + mColorHl = colorHl; + mColorBalloon = colorBalloon; + } +} diff --git a/src/com/android/inputmethod/pinyin/SoftKey.java b/src/com/android/inputmethod/pinyin/SoftKey.java new file mode 100644 index 0000000..67eaf29 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/SoftKey.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import android.graphics.drawable.Drawable; + +/** + * Class for soft keys which defined in the keyboard xml file. A soft key can be + * a basic key or a toggling key. + * + * @see com.android.inputmethod.pinyin.SoftKeyToggle + */ +public class SoftKey { + protected static final int KEYMASK_REPEAT = 0x10000000; + protected static final int KEYMASK_BALLOON = 0x20000000; + + /** + * For a finger touch device, after user presses a key, there will be some + * consequent moving events because of the changing in touching pressure. If + * the moving distance in x is within this threshold, the moving events will + * be ignored. + */ + public static final int MAX_MOVE_TOLERANCE_X = 0; + + /** + * For a finger touch device, after user presses a key, there will be some + * consequent moving events because of the changing in touching pressure. If + * the moving distance in y is within this threshold, the moving events will + * be ignored. + */ + public static final int MAX_MOVE_TOLERANCE_Y = 0; + + /** + * Used to indicate the type and attributes of this key. the lowest 8 bits + * should be reserved for SoftkeyToggle. + */ + protected int mKeyMask; + + protected SoftKeyType mKeyType; + + protected Drawable mKeyIcon; + + protected Drawable mKeyIconPopup; + + protected String mKeyLabel; + + protected int mKeyCode; + + /** + * If this value is not 0, this key can be used to popup a sub soft keyboard + * when user presses it for some time. + */ + public int mPopupSkbId; + + public float mLeftF; + public float mRightF; + public float mTopF; + public float mBottomF; + public int mLeft; + public int mRight; + public int mTop; + public int mBottom; + + public void setKeyType(SoftKeyType keyType, Drawable keyIcon, + Drawable keyIconPopup) { + mKeyType = keyType; + mKeyIcon = keyIcon; + mKeyIconPopup = keyIconPopup; + } + + // The caller guarantees that all parameters are in [0, 1] + public void setKeyDimensions(float left, float top, float right, + float bottom) { + mLeftF = left; + mTopF = top; + mRightF = right; + mBottomF = bottom; + } + + public void setKeyAttribute(int keyCode, String label, boolean repeat, + boolean balloon) { + mKeyCode = keyCode; + mKeyLabel = label; + + if (repeat) { + mKeyMask |= KEYMASK_REPEAT; + } else { + mKeyMask &= (~KEYMASK_REPEAT); + } + + if (balloon) { + mKeyMask |= KEYMASK_BALLOON; + } else { + mKeyMask &= (~KEYMASK_BALLOON); + } + } + + public void setPopupSkbId(int popupSkbId) { + mPopupSkbId = popupSkbId; + } + + // Call after setKeyDimensions(). The caller guarantees that the + // keyboard with and height are valid. + public void setSkbCoreSize(int skbWidth, int skbHeight) { + mLeft = (int) (mLeftF * skbWidth); + mRight = (int) (mRightF * skbWidth); + mTop = (int) (mTopF * skbHeight); + mBottom = (int) (mBottomF * skbHeight); + } + + public Drawable getKeyIcon() { + return mKeyIcon; + } + + public Drawable getKeyIconPopup() { + if (null != mKeyIconPopup) { + return mKeyIconPopup; + } + return mKeyIcon; + } + + public int getKeyCode() { + return mKeyCode; + } + + public String getKeyLabel() { + return mKeyLabel; + } + + public void changeCase(boolean upperCase) { + if (null != mKeyLabel) { + if (upperCase) + mKeyLabel = mKeyLabel.toUpperCase(); + else + mKeyLabel = mKeyLabel.toLowerCase(); + } + } + + public Drawable getKeyBg() { + return mKeyType.mKeyBg; + } + + public Drawable getKeyHlBg() { + return mKeyType.mKeyHlBg; + } + + public int getColor() { + return mKeyType.mColor; + } + + public int getColorHl() { + return mKeyType.mColorHl; + } + + public int getColorBalloon() { + return mKeyType.mColorBalloon; + } + + public boolean isKeyCodeKey() { + if (mKeyCode > 0) return true; + return false; + } + + public boolean isUserDefKey() { + if (mKeyCode < 0) return true; + return false; + } + + public boolean isUniStrKey() { + if (null != mKeyLabel && mKeyCode == 0) return true; + return false; + } + + public boolean needBalloon() { + return (mKeyMask & KEYMASK_BALLOON) != 0; + } + + public boolean repeatable() { + return (mKeyMask & KEYMASK_REPEAT) != 0; + } + + public int getPopupResId() { + return mPopupSkbId; + } + + public int width() { + return mRight - mLeft; + } + + public int height() { + return mBottom - mTop; + } + + public boolean moveWithinKey(int x, int y) { + if (mLeft - MAX_MOVE_TOLERANCE_X <= x + && mTop - MAX_MOVE_TOLERANCE_Y <= y + && mRight + MAX_MOVE_TOLERANCE_X > x + && mBottom + MAX_MOVE_TOLERANCE_Y > y) { + return true; + } + return false; + } + + @Override + public String toString() { + String str = "\n"; + str += " keyCode: " + String.valueOf(mKeyCode) + "\n"; + str += " keyMask: " + String.valueOf(mKeyMask) + "\n"; + str += " keyLabel: " + (mKeyLabel == null ? "null" : mKeyLabel) + "\n"; + str += " popupResId: " + String.valueOf(mPopupSkbId) + "\n"; + str += " Position: " + String.valueOf(mLeftF) + ", " + + String.valueOf(mTopF) + ", " + String.valueOf(mRightF) + ", " + + String.valueOf(mBottomF) + "\n"; + return str; + } +} diff --git a/src/com/android/inputmethod/pinyin/SoftKeyToggle.java b/src/com/android/inputmethod/pinyin/SoftKeyToggle.java new file mode 100644 index 0000000..89ff2fe --- /dev/null +++ b/src/com/android/inputmethod/pinyin/SoftKeyToggle.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import android.graphics.drawable.Drawable; + +/** + * Class for soft keys which defined in the keyboard xml file. A soft key can be + * a basic key or a toggling key. + * + * @see com.android.inputmethod.pinyin.SoftKey + */ +public class SoftKeyToggle extends SoftKey { + /** + * The current state number is stored in the lowest 8 bits of mKeyMask, this + * mask is used to get the state number. If the current state is 0, the + * normal state is enabled; if the current state is more than 0, a toggle + * state in the toggle state chain will be enabled. + */ + private static final int KEYMASK_TOGGLE_STATE = 0x000000ff; + + private ToggleState mToggleState; + + public int getToggleStateId() { + return (mKeyMask & KEYMASK_TOGGLE_STATE); + } + + // The state id should be valid, and less than 255. + // If resetIfNotFound is true and there is no such toggle state with the + // given id, the key state will be reset. + // If the key state is newly changed (enabled to the given state, or + // reseted) and needs re-draw, return true. + public boolean enableToggleState(int stateId, boolean resetIfNotFound) { + int oldStateId = (mKeyMask & KEYMASK_TOGGLE_STATE); + if (oldStateId == stateId) return false; + + mKeyMask &= (~KEYMASK_TOGGLE_STATE); + if (stateId > 0) { + mKeyMask |= (KEYMASK_TOGGLE_STATE & stateId); + if (getToggleState() == null) { + mKeyMask &= (~KEYMASK_TOGGLE_STATE); + if (!resetIfNotFound && oldStateId > 0) { + mKeyMask |= (KEYMASK_TOGGLE_STATE & oldStateId); + } + return resetIfNotFound; + } else { + return true; + } + } else { + return true; + } + } + + // The state id should be valid, and less than 255. + // If resetIfNotFound is true and there is no such toggle state with the + // given id, the key state will be reset. + // If the key state is newly changed and needs re-draw, return true. + public boolean disableToggleState(int stateId, boolean resetIfNotFound) { + int oldStateId = (mKeyMask & KEYMASK_TOGGLE_STATE); + if (oldStateId == stateId) { + mKeyMask &= (~KEYMASK_TOGGLE_STATE); + return stateId != 0; + } + + if (resetIfNotFound) { + mKeyMask &= (~KEYMASK_TOGGLE_STATE); + return oldStateId != 0; + } + return false; + } + + // Clear any toggle state. If the key needs re-draw, return true. + public boolean disableAllToggleStates() { + int oldStateId = (mKeyMask & KEYMASK_TOGGLE_STATE); + mKeyMask &= (~KEYMASK_TOGGLE_STATE); + return oldStateId != 0; + } + + @Override + public Drawable getKeyIcon() { + ToggleState state = getToggleState(); + if (null != state) return state.mKeyIcon; + return super.getKeyIcon(); + } + + @Override + public Drawable getKeyIconPopup() { + ToggleState state = getToggleState(); + if (null != state) { + if (null != state.mKeyIconPopup) { + return state.mKeyIconPopup; + } else { + return state.mKeyIcon; + } + } + return super.getKeyIconPopup(); + } + + @Override + public int getKeyCode() { + ToggleState state = getToggleState(); + if (null != state) return state.mKeyCode; + return mKeyCode; + } + + @Override + public String getKeyLabel() { + ToggleState state = getToggleState(); + if (null != state) return state.mKeyLabel; + return mKeyLabel; + } + + @Override + public Drawable getKeyBg() { + ToggleState state = getToggleState(); + if (null != state && null != state.mKeyType) { + return state.mKeyType.mKeyBg; + } + return mKeyType.mKeyBg; + } + + @Override + public Drawable getKeyHlBg() { + ToggleState state = getToggleState(); + if (null != state && null != state.mKeyType) { + return state.mKeyType.mKeyHlBg; + } + return mKeyType.mKeyHlBg; + } + + @Override + public int getColor() { + ToggleState state = getToggleState(); + if (null != state && null != state.mKeyType) { + return state.mKeyType.mColor; + } + return mKeyType.mColor; + } + + @Override + public int getColorHl() { + ToggleState state = getToggleState(); + if (null != state && null != state.mKeyType) { + return state.mKeyType.mColorHl; + } + return mKeyType.mColorHl; + } + + @Override + public int getColorBalloon() { + ToggleState state = getToggleState(); + if (null != state && null != state.mKeyType) { + return state.mKeyType.mColorBalloon; + } + return mKeyType.mColorBalloon; + } + + @Override + public boolean isKeyCodeKey() { + ToggleState state = getToggleState(); + if (null != state) { + if (state.mKeyCode > 0) return true; + return false; + } + return super.isKeyCodeKey(); + } + + @Override + public boolean isUserDefKey() { + ToggleState state = getToggleState(); + if (null != state) { + if (state.mKeyCode < 0) return true; + return false; + } + return super.isUserDefKey(); + } + + @Override + public boolean isUniStrKey() { + ToggleState state = getToggleState(); + if (null != state) { + if (null != state.mKeyLabel && state.mKeyCode == 0) { + return true; + } + return false; + } + return super.isUniStrKey(); + } + + @Override + public boolean needBalloon() { + ToggleState state = getToggleState(); + if (null != state) { + return (state.mIdAndFlags & KEYMASK_BALLOON) != 0; + } + return super.needBalloon(); + } + + @Override + public boolean repeatable() { + ToggleState state = getToggleState(); + if (null != state) { + return (state.mIdAndFlags & KEYMASK_REPEAT) != 0; + } + return super.repeatable(); + } + + @Override + public void changeCase(boolean lowerCase) { + ToggleState state = getToggleState(); + if (null != state && null != state.mKeyLabel) { + if (lowerCase) + state.mKeyLabel = state.mKeyLabel.toLowerCase(); + else + state.mKeyLabel = state.mKeyLabel.toUpperCase(); + } + } + + public ToggleState createToggleState() { + return new ToggleState(); + } + + public boolean setToggleStates(ToggleState rootState) { + if (null == rootState) return false; + mToggleState = rootState; + return true; + } + + private ToggleState getToggleState() { + int stateId = (mKeyMask & KEYMASK_TOGGLE_STATE); + if (0 == stateId) return null; + + ToggleState state = mToggleState; + while ((null != state) + && (state.mIdAndFlags & KEYMASK_TOGGLE_STATE) != stateId) { + state = state.mNextState; + } + return state; + } + + public class ToggleState { + // The id should be bigger than 0; + private int mIdAndFlags; + public SoftKeyType mKeyType; + public int mKeyCode; + public Drawable mKeyIcon; + public Drawable mKeyIconPopup; + public String mKeyLabel; + public ToggleState mNextState; + + public void setStateId(int stateId) { + mIdAndFlags |= (stateId & KEYMASK_TOGGLE_STATE); + } + + public void setStateFlags(boolean repeat, boolean balloon) { + if (repeat) { + mIdAndFlags |= KEYMASK_REPEAT; + } else { + mIdAndFlags &= (~KEYMASK_REPEAT); + } + + if (balloon) { + mIdAndFlags |= KEYMASK_BALLOON; + } else { + mIdAndFlags &= (~KEYMASK_BALLOON); + } + } + } +} diff --git a/src/com/android/inputmethod/pinyin/SoftKeyboard.java b/src/com/android/inputmethod/pinyin/SoftKeyboard.java new file mode 100644 index 0000000..b8cc504 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/SoftKeyboard.java @@ -0,0 +1,520 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import com.android.inputmethod.pinyin.InputModeSwitcher.ToggleStates; + +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.KeyEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class used to represent a soft keyboard definition, including the height, the + * background image, the image for high light, the keys, etc. + */ +public class SoftKeyboard { + /** The XML resource id for this soft keyboard. */ + private int mSkbXmlId; + + /** Do we need to cache this soft keyboard? */ + private boolean mCacheFlag; + + /** + * After user switches to this soft keyboard, if this flag is true, this + * soft keyboard will be kept unless explicit switching operation is + * performed, otherwise IME will switch back to the previous keyboard layout + * whenever user clicks on any none-function key. + **/ + private boolean mStickyFlag; + + /** + * The cache id for this soft keyboard. It is used to identify it in the + * soft keyboard pool. + */ + private int mCacheId; + + /** + * Used to indicate whether this soft keyboard is newly loaded from an XML + * file or is just gotten from the soft keyboard pool. + */ + private boolean mNewlyLoadedFlag = true; + + /** The width of the soft keyboard. */ + private int mSkbCoreWidth; + + /** The height of the soft keyboard. */ + private int mSkbCoreHeight; + + /** The soft keyboard template for this soft keyboard. */ + private SkbTemplate mSkbTemplate; + + /** Used to indicate whether this soft keyboard is a QWERTY keyboard. */ + private boolean mIsQwerty; + + /** + * When {@link #mIsQwerty} is true, this member is Used to indicate that the + * soft keyboard should be displayed in uppercase. + */ + private boolean mIsQwertyUpperCase; + + /** + * The id of the rows which are enabled. Rows with id + * {@link KeyRow#ALWAYS_SHOW_ROW_ID} are always enabled. + */ + private int mEnabledRowId; + + /** + * Rows in this soft keyboard. Each row has a id. Only matched rows will be + * enabled. + */ + private List<KeyRow> mKeyRows; + + /** + * Background of the soft keyboard. If it is null, the one in the soft + * keyboard template will be used. + **/ + public Drawable mSkbBg; + + /** + * Background for key balloon. If it is null, the one in the soft keyboard + * template will be used. + **/ + private Drawable mBalloonBg; + + /** + * Background for popup mini soft keyboard. If it is null, the one in the + * soft keyboard template will be used. + **/ + private Drawable mPopupBg; + + /** The left and right margin of a key. */ + private float mKeyXMargin = 0; + + /** The top and bottom margin of a key. */ + private float mKeyYMargin = 0; + + private Rect mTmpRect = new Rect(); + + public SoftKeyboard(int skbXmlId, SkbTemplate skbTemplate, int skbWidth, + int skbHeight) { + mSkbXmlId = skbXmlId; + mSkbTemplate = skbTemplate; + mSkbCoreWidth = skbWidth; + mSkbCoreHeight = skbHeight; + } + + public void setFlags(boolean cacheFlag, boolean stickyFlag, + boolean isQwerty, boolean isQwertyUpperCase) { + mCacheFlag = cacheFlag; + mStickyFlag = stickyFlag; + mIsQwerty = isQwerty; + mIsQwertyUpperCase = isQwertyUpperCase; + } + + public boolean getCacheFlag() { + return mCacheFlag; + } + + public void setCacheId(int cacheId) { + mCacheId = cacheId; + } + + public boolean getStickyFlag() { + return mStickyFlag; + } + + public void setSkbBackground(Drawable skbBg) { + mSkbBg = skbBg; + } + + public void setPopupBackground(Drawable popupBg) { + mPopupBg = popupBg; + } + + public void setKeyBalloonBackground(Drawable balloonBg) { + mBalloonBg = balloonBg; + } + + public void setKeyMargins(float xMargin, float yMargin) { + mKeyXMargin = xMargin; + mKeyYMargin = yMargin; + } + + public int getCacheId() { + return mCacheId; + } + + public void reset() { + if (null != mKeyRows) mKeyRows.clear(); + } + + public void setNewlyLoadedFlag(boolean newlyLoadedFlag) { + mNewlyLoadedFlag = newlyLoadedFlag; + } + + public boolean getNewlyLoadedFlag() { + return mNewlyLoadedFlag; + } + + public void beginNewRow(int rowId, float yStartingPos) { + if (null == mKeyRows) mKeyRows = new ArrayList<KeyRow>(); + KeyRow keyRow = new KeyRow(); + keyRow.mRowId = rowId; + keyRow.mTopF = yStartingPos; + keyRow.mBottomF = yStartingPos; + keyRow.mSoftKeys = new ArrayList<SoftKey>(); + mKeyRows.add(keyRow); + } + + public boolean addSoftKey(SoftKey softKey) { + if (mKeyRows.size() == 0) return false; + KeyRow keyRow = mKeyRows.get(mKeyRows.size() - 1); + if (null == keyRow) return false; + List<SoftKey> softKeys = keyRow.mSoftKeys; + + softKey.setSkbCoreSize(mSkbCoreWidth, mSkbCoreHeight); + softKeys.add(softKey); + if (softKey.mTopF < keyRow.mTopF) { + keyRow.mTopF = softKey.mTopF; + } + if (softKey.mBottomF > keyRow.mBottomF) { + keyRow.mBottomF = softKey.mBottomF; + } + return true; + } + + public int getSkbXmlId() { + return mSkbXmlId; + } + + // Set the size of the soft keyboard core. In other words, the background's + // padding are not counted. + public void setSkbCoreSize(int skbCoreWidth, int skbCoreHeight) { + if (null == mKeyRows + || (skbCoreWidth == mSkbCoreWidth && skbCoreHeight == mSkbCoreHeight)) { + return; + } + for (int row = 0; row < mKeyRows.size(); row++) { + KeyRow keyRow = mKeyRows.get(row); + keyRow.mBottom = (int) (skbCoreHeight * keyRow.mBottomF); + keyRow.mTop = (int) (skbCoreHeight * keyRow.mTopF); + + List<SoftKey> softKeys = keyRow.mSoftKeys; + for (int i = 0; i < softKeys.size(); i++) { + SoftKey softKey = softKeys.get(i); + softKey.setSkbCoreSize(skbCoreWidth, skbCoreHeight); + } + } + mSkbCoreWidth = skbCoreWidth; + mSkbCoreHeight = skbCoreHeight; + } + + public int getSkbCoreWidth() { + return mSkbCoreWidth; + } + + public int getSkbCoreHeight() { + return mSkbCoreHeight; + } + + public int getSkbTotalWidth() { + Rect padding = getPadding(); + return mSkbCoreWidth + padding.left + padding.right; + } + + public int getSkbTotalHeight() { + Rect padding = getPadding(); + return mSkbCoreHeight + padding.top + padding.bottom; + } + + public int getKeyXMargin() { + Environment env = Environment.getInstance(); + return (int) (mKeyXMargin * mSkbCoreWidth * env.getKeyXMarginFactor()); + } + + public int getKeyYMargin() { + Environment env = Environment.getInstance(); + return (int) (mKeyYMargin * mSkbCoreHeight * env.getKeyYMarginFactor()); + } + + public Drawable getSkbBackground() { + if (null != mSkbBg) return mSkbBg; + return mSkbTemplate.getSkbBackground(); + } + + public Drawable getBalloonBackground() { + if (null != mBalloonBg) return mBalloonBg; + return mSkbTemplate.getBalloonBackground(); + } + + public Drawable getPopupBackground() { + if (null != mPopupBg) return mPopupBg; + return mSkbTemplate.getPopupBackground(); + } + + public int getRowNum() { + if (null != mKeyRows) { + return mKeyRows.size(); + } + return 0; + } + + public KeyRow getKeyRowForDisplay(int row) { + if (null != mKeyRows && mKeyRows.size() > row) { + KeyRow keyRow = mKeyRows.get(row); + if (KeyRow.ALWAYS_SHOW_ROW_ID == keyRow.mRowId + || keyRow.mRowId == mEnabledRowId) { + return keyRow; + } + } + return null; + } + + public SoftKey getKey(int row, int location) { + if (null != mKeyRows && mKeyRows.size() > row) { + List<SoftKey> softKeys = mKeyRows.get(row).mSoftKeys; + if (softKeys.size() > location) { + return softKeys.get(location); + } + } + return null; + } + + public SoftKey mapToKey(int x, int y) { + if (null == mKeyRows) { + return null; + } + // If the position is inside the rectangle of a certain key, return that + // key. + int rowNum = mKeyRows.size(); + for (int row = 0; row < rowNum; row++) { + KeyRow keyRow = mKeyRows.get(row); + if (KeyRow.ALWAYS_SHOW_ROW_ID != keyRow.mRowId + && keyRow.mRowId != mEnabledRowId) continue; + if (keyRow.mTop > y && keyRow.mBottom <= y) continue; + + List<SoftKey> softKeys = keyRow.mSoftKeys; + int keyNum = softKeys.size(); + for (int i = 0; i < keyNum; i++) { + SoftKey sKey = softKeys.get(i); + if (sKey.mLeft <= x && sKey.mTop <= y && sKey.mRight > x + && sKey.mBottom > y) { + return sKey; + } + } + } + + // If the position is outside the rectangles of all keys, find the + // nearest one. + SoftKey nearestKey = null; + float nearestDis = Float.MAX_VALUE; + for (int row = 0; row < rowNum; row++) { + KeyRow keyRow = mKeyRows.get(row); + if (KeyRow.ALWAYS_SHOW_ROW_ID != keyRow.mRowId + && keyRow.mRowId != mEnabledRowId) continue; + if (keyRow.mTop > y && keyRow.mBottom <= y) continue; + + List<SoftKey> softKeys = keyRow.mSoftKeys; + int keyNum = softKeys.size(); + for (int i = 0; i < keyNum; i++) { + SoftKey sKey = softKeys.get(i); + int disx = (sKey.mLeft + sKey.mRight) / 2 - x; + int disy = (sKey.mTop + sKey.mBottom) / 2 - y; + float dis = disx * disx + disy * disy; + if (dis < nearestDis) { + nearestDis = dis; + nearestKey = sKey; + } + } + } + return nearestKey; + } + + public void switchQwertyMode(int toggle_state_id, boolean upperCase) { + if (!mIsQwerty) return; + + int rowNum = mKeyRows.size(); + for (int row = 0; row < rowNum; row++) { + KeyRow keyRow = mKeyRows.get(row); + List<SoftKey> softKeys = keyRow.mSoftKeys; + int keyNum = softKeys.size(); + for (int i = 0; i < keyNum; i++) { + SoftKey sKey = softKeys.get(i); + if (sKey instanceof SoftKeyToggle) { + ((SoftKeyToggle) sKey).enableToggleState(toggle_state_id, + true); + } + if (sKey.mKeyCode >= KeyEvent.KEYCODE_A + && sKey.mKeyCode <= KeyEvent.KEYCODE_Z) { + sKey.changeCase(upperCase); + } + } + } + } + + public void enableToggleState(int toggleStateId, boolean resetIfNotFound) { + int rowNum = mKeyRows.size(); + for (int row = 0; row < rowNum; row++) { + KeyRow keyRow = mKeyRows.get(row); + List<SoftKey> softKeys = keyRow.mSoftKeys; + int keyNum = softKeys.size(); + for (int i = 0; i < keyNum; i++) { + SoftKey sKey = softKeys.get(i); + if (sKey instanceof SoftKeyToggle) { + ((SoftKeyToggle) sKey).enableToggleState(toggleStateId, + resetIfNotFound); + } + } + } + } + + public void disableToggleState(int toggleStateId, boolean resetIfNotFound) { + int rowNum = mKeyRows.size(); + for (int row = 0; row < rowNum; row++) { + KeyRow keyRow = mKeyRows.get(row); + List<SoftKey> softKeys = keyRow.mSoftKeys; + int keyNum = softKeys.size(); + for (int i = 0; i < keyNum; i++) { + SoftKey sKey = softKeys.get(i); + if (sKey instanceof SoftKeyToggle) { + ((SoftKeyToggle) sKey).disableToggleState(toggleStateId, + resetIfNotFound); + } + } + } + } + + public void enableToggleStates(ToggleStates toggleStates) { + if (null == toggleStates) return; + + enableRow(toggleStates.mRowIdToEnable); + + boolean isQwerty = toggleStates.mQwerty; + boolean isQwertyUpperCase = toggleStates.mQwertyUpperCase; + boolean needUpdateQwerty = (isQwerty && mIsQwerty && (mIsQwertyUpperCase != isQwertyUpperCase)); + int states[] = toggleStates.mKeyStates; + int statesNum = toggleStates.mKeyStatesNum; + + int rowNum = mKeyRows.size(); + for (int row = 0; row < rowNum; row++) { + KeyRow keyRow = mKeyRows.get(row); + if (KeyRow.ALWAYS_SHOW_ROW_ID != keyRow.mRowId + && keyRow.mRowId != mEnabledRowId) { + continue; + } + List<SoftKey> softKeys = keyRow.mSoftKeys; + int keyNum = softKeys.size(); + for (int keyPos = 0; keyPos < keyNum; keyPos++) { + SoftKey sKey = softKeys.get(keyPos); + if (sKey instanceof SoftKeyToggle) { + for (int statePos = 0; statePos < statesNum; statePos++) { + ((SoftKeyToggle) sKey).enableToggleState( + states[statePos], statePos == 0); + } + if (0 == statesNum) { + ((SoftKeyToggle) sKey).disableAllToggleStates(); + } + } + if (needUpdateQwerty) { + if (sKey.mKeyCode >= KeyEvent.KEYCODE_A + && sKey.mKeyCode <= KeyEvent.KEYCODE_Z) { + sKey.changeCase(isQwertyUpperCase); + } + } + } + } + mIsQwertyUpperCase = isQwertyUpperCase; + } + + private Rect getPadding() { + mTmpRect.set(0, 0, 0, 0); + Drawable skbBg = getSkbBackground(); + if (null == skbBg) return mTmpRect; + skbBg.getPadding(mTmpRect); + return mTmpRect; + } + + /** + * Enable a row with the give toggle Id. Rows with other toggle ids (except + * the id {@link KeyRow#ALWAYS_SHOW_ROW_ID}) will be disabled. + * + * @param rowId The row id to enable. + * @return True if the soft keyboard requires redrawing. + */ + private boolean enableRow(int rowId) { + if (KeyRow.ALWAYS_SHOW_ROW_ID == rowId) return false; + + boolean enabled = false; + int rowNum = mKeyRows.size(); + for (int row = rowNum - 1; row >= 0; row--) { + if (mKeyRows.get(row).mRowId == rowId) { + enabled = true; + break; + } + } + if (enabled) { + mEnabledRowId = rowId; + } + return enabled; + } + + @Override + public String toString() { + String str = "------------------SkbInfo----------------------\n"; + String endStr = "-----------------------------------------------\n"; + str += "Width: " + String.valueOf(mSkbCoreWidth) + "\n"; + str += "Height: " + String.valueOf(mSkbCoreHeight) + "\n"; + str += "KeyRowNum: " + mKeyRows == null ? "0" : String.valueOf(mKeyRows + .size()) + + "\n"; + if (null == mKeyRows) return str + endStr; + int rowNum = mKeyRows.size(); + for (int row = 0; row < rowNum; row++) { + KeyRow keyRow = mKeyRows.get(row); + List<SoftKey> softKeys = keyRow.mSoftKeys; + int keyNum = softKeys.size(); + for (int i = 0; i < softKeys.size(); i++) { + str += "-key " + String.valueOf(i) + ":" + + softKeys.get(i).toString(); + } + } + return str + endStr; + } + + public String toShortString() { + return super.toString(); + } + + class KeyRow { + static final int ALWAYS_SHOW_ROW_ID = -1; + static final int DEFAULT_ROW_ID = 0; + + List<SoftKey> mSoftKeys; + /** + * If the row id is {@link #ALWAYS_SHOW_ROW_ID}, this row will always be + * enabled. + */ + int mRowId; + float mTopF; + float mBottomF; + int mTop; + int mBottom; + } +} diff --git a/src/com/android/inputmethod/pinyin/SoftKeyboardView.java b/src/com/android/inputmethod/pinyin/SoftKeyboardView.java new file mode 100644 index 0000000..5543f33 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/SoftKeyboardView.java @@ -0,0 +1,478 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import com.android.inputmethod.pinyin.SoftKeyboard.KeyRow; + +import java.util.List; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Paint.FontMetricsInt; +import android.graphics.drawable.Drawable; +import android.os.Vibrator; +import android.util.AttributeSet; +import android.view.View; + +/** + * Class used to show a soft keyboard. + * + * A soft keyboard view should not handle touch event itself, because we do bias + * correction, need a global strategy to map an event into a proper view to + * achieve better user experience. + */ +public class SoftKeyboardView extends View { + /** + * The definition of the soft keyboard for the current this soft keyboard + * view. + */ + private SoftKeyboard mSoftKeyboard; + + /** + * The popup balloon hint for key press/release. + */ + private BalloonHint mBalloonPopup; + + /** + * The on-key balloon hint for key press/release. If it is null, on-key + * highlight will be drawn on th soft keyboard view directly. + */ + private BalloonHint mBalloonOnKey; + + /** Used to play key sounds. */ + private SoundManager mSoundManager; + + /** The last key pressed. */ + private SoftKey mSoftKeyDown; + + /** Used to indicate whether the user is holding on a key. */ + private boolean mKeyPressed = false; + + /** + * The location offset of the view to the keyboard container. + */ + private int mOffsetToSkbContainer[] = new int[2]; + + /** + * The location of the desired hint view to the keyboard container. + */ + private int mHintLocationToSkbContainer[] = new int[2]; + + /** + * Text size for normal key. + */ + private int mNormalKeyTextSize; + + /** + * Text size for function key. + */ + private int mFunctionKeyTextSize; + + /** + * Long press timer used to response long-press. + */ + private SkbContainer.LongPressTimer mLongPressTimer; + + /** + * Repeated events for long press + */ + private boolean mRepeatForLongPress = false; + + /** + * If this parameter is true, the balloon will never be dismissed even if + * user moves a lot from the pressed point. + */ + private boolean mMovingNeverHidePopupBalloon = false; + + /** Vibration for key press. */ + private Vibrator mVibrator; + + /** Vibration pattern for key press. */ + protected long[] mVibratePattern = new long[] {1, 20}; + + /** + * The dirty rectangle used to mark the area to re-draw during key press and + * release. Currently, whenever we can invalidate(Rect), view will call + * onDraw() and we MUST draw the whole view. This dirty information is for + * future use. + */ + private Rect mDirtyRect = new Rect(); + + private Paint mPaint; + private FontMetricsInt mFmi; + private boolean mDimSkb; + + public SoftKeyboardView(Context context, AttributeSet attrs) { + super(context, attrs); + + mSoundManager = SoundManager.getInstance(mContext); + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mFmi = mPaint.getFontMetricsInt(); + } + + public boolean setSoftKeyboard(SoftKeyboard softSkb) { + if (null == softSkb) { + return false; + } + mSoftKeyboard = softSkb; + Drawable bg = softSkb.getSkbBackground(); + if (null != bg) setBackgroundDrawable(bg); + return true; + } + + public SoftKeyboard getSoftKeyboard() { + return mSoftKeyboard; + } + + public void resizeKeyboard(int skbWidth, int skbHeight) { + mSoftKeyboard.setSkbCoreSize(skbWidth, skbHeight); + } + + public void setBalloonHint(BalloonHint balloonOnKey, + BalloonHint balloonPopup, boolean movingNeverHidePopup) { + mBalloonOnKey = balloonOnKey; + mBalloonPopup = balloonPopup; + mMovingNeverHidePopupBalloon = movingNeverHidePopup; + } + + public void setOffsetToSkbContainer(int offsetToSkbContainer[]) { + mOffsetToSkbContainer[0] = offsetToSkbContainer[0]; + mOffsetToSkbContainer[1] = offsetToSkbContainer[1]; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int measuredWidth = 0; + int measuredHeight = 0; + if (null != mSoftKeyboard) { + measuredWidth = mSoftKeyboard.getSkbCoreWidth(); + measuredHeight = mSoftKeyboard.getSkbCoreHeight(); + measuredWidth += mPaddingLeft + mPaddingRight; + measuredHeight += mPaddingTop + mPaddingBottom; + } + setMeasuredDimension(measuredWidth, measuredHeight); + } + + private void showBalloon(BalloonHint balloon, int balloonLocationToSkb[], + boolean movePress) { + long delay = BalloonHint.TIME_DELAY_SHOW; + if (movePress) delay = 0; + if (balloon.needForceDismiss()) { + balloon.delayedDismiss(0); + } + if (!balloon.isShowing()) { + balloon.delayedShow(delay, balloonLocationToSkb); + } else { + balloon.delayedUpdate(delay, balloonLocationToSkb, balloon + .getWidth(), balloon.getHeight()); + } + long b = System.currentTimeMillis(); + } + + public void resetKeyPress(long balloonDelay) { + if (!mKeyPressed) return; + mKeyPressed = false; + if (null != mBalloonOnKey) { + mBalloonOnKey.delayedDismiss(balloonDelay); + } else { + if (null != mSoftKeyDown) { + if (mDirtyRect.isEmpty()) { + mDirtyRect.set(mSoftKeyDown.mLeft, mSoftKeyDown.mTop, + mSoftKeyDown.mRight, mSoftKeyDown.mBottom); + } + invalidate(mDirtyRect); + } else { + invalidate(); + } + } + mBalloonPopup.delayedDismiss(balloonDelay); + } + + // If movePress is true, means that this function is called because user + // moves his finger to this button. If movePress is false, means that this + // function is called when user just presses this key. + public SoftKey onKeyPress(int x, int y, + SkbContainer.LongPressTimer longPressTimer, boolean movePress) { + mKeyPressed = false; + boolean moveWithinPreviousKey = false; + if (movePress) { + SoftKey newKey = mSoftKeyboard.mapToKey(x, y); + if (newKey == mSoftKeyDown) moveWithinPreviousKey = true; + mSoftKeyDown = newKey; + } else { + mSoftKeyDown = mSoftKeyboard.mapToKey(x, y); + } + if (moveWithinPreviousKey || null == mSoftKeyDown) return mSoftKeyDown; + mKeyPressed = true; + + if (!movePress) { + tryPlayKeyDown(); + tryVibrate(); + } + + mLongPressTimer = longPressTimer; + + if (!movePress) { + if (mSoftKeyDown.getPopupResId() > 0 || mSoftKeyDown.repeatable()) { + mLongPressTimer.startTimer(); + } + } else { + mLongPressTimer.removeTimer(); + } + + int desired_width; + int desired_height; + float textSize; + Environment env = Environment.getInstance(); + + if (null != mBalloonOnKey) { + Drawable keyHlBg = mSoftKeyDown.getKeyHlBg(); + mBalloonOnKey.setBalloonBackground(keyHlBg); + + // Prepare the on-key balloon + int keyXMargin = mSoftKeyboard.getKeyXMargin(); + int keyYMargin = mSoftKeyboard.getKeyYMargin(); + desired_width = mSoftKeyDown.width() - 2 * keyXMargin; + desired_height = mSoftKeyDown.height() - 2 * keyYMargin; + textSize = env + .getKeyTextSize(SoftKeyType.KEYTYPE_ID_NORMAL_KEY != mSoftKeyDown.mKeyType.mKeyTypeId); + Drawable icon = mSoftKeyDown.getKeyIcon(); + if (null != icon) { + mBalloonOnKey.setBalloonConfig(icon, desired_width, + desired_height); + } else { + mBalloonOnKey.setBalloonConfig(mSoftKeyDown.getKeyLabel(), + textSize, true, mSoftKeyDown.getColorHl(), + desired_width, desired_height); + } + + mHintLocationToSkbContainer[0] = mPaddingLeft + mSoftKeyDown.mLeft + - (mBalloonOnKey.getWidth() - mSoftKeyDown.width()) / 2; + mHintLocationToSkbContainer[0] += mOffsetToSkbContainer[0]; + mHintLocationToSkbContainer[1] = mPaddingTop + + (mSoftKeyDown.mBottom - keyYMargin) + - mBalloonOnKey.getHeight(); + mHintLocationToSkbContainer[1] += mOffsetToSkbContainer[1]; + showBalloon(mBalloonOnKey, mHintLocationToSkbContainer, movePress); + } else { + mDirtyRect.union(mSoftKeyDown.mLeft, mSoftKeyDown.mTop, + mSoftKeyDown.mRight, mSoftKeyDown.mBottom); + invalidate(mDirtyRect); + } + + // Prepare the popup balloon + if (mSoftKeyDown.needBalloon()) { + Drawable balloonBg = mSoftKeyboard.getBalloonBackground(); + mBalloonPopup.setBalloonBackground(balloonBg); + + desired_width = mSoftKeyDown.width() + env.getKeyBalloonWidthPlus(); + desired_height = mSoftKeyDown.height() + + env.getKeyBalloonHeightPlus(); + textSize = env + .getBalloonTextSize(SoftKeyType.KEYTYPE_ID_NORMAL_KEY != mSoftKeyDown.mKeyType.mKeyTypeId); + Drawable iconPopup = mSoftKeyDown.getKeyIconPopup(); + if (null != iconPopup) { + mBalloonPopup.setBalloonConfig(iconPopup, desired_width, + desired_height); + } else { + mBalloonPopup.setBalloonConfig(mSoftKeyDown.getKeyLabel(), + textSize, mSoftKeyDown.needBalloon(), mSoftKeyDown + .getColorBalloon(), desired_width, + desired_height); + } + + // The position to show. + mHintLocationToSkbContainer[0] = mPaddingLeft + mSoftKeyDown.mLeft + + -(mBalloonPopup.getWidth() - mSoftKeyDown.width()) / 2; + mHintLocationToSkbContainer[0] += mOffsetToSkbContainer[0]; + mHintLocationToSkbContainer[1] = mPaddingTop + mSoftKeyDown.mTop + - mBalloonPopup.getHeight(); + mHintLocationToSkbContainer[1] += mOffsetToSkbContainer[1]; + showBalloon(mBalloonPopup, mHintLocationToSkbContainer, movePress); + } else { + mBalloonPopup.delayedDismiss(0); + } + + if (mRepeatForLongPress) longPressTimer.startTimer(); + return mSoftKeyDown; + } + + public SoftKey onKeyRelease(int x, int y) { + mKeyPressed = false; + if (null == mSoftKeyDown) return null; + + mLongPressTimer.removeTimer(); + + if (null != mBalloonOnKey) { + mBalloonOnKey.delayedDismiss(BalloonHint.TIME_DELAY_DISMISS); + } else { + mDirtyRect.union(mSoftKeyDown.mLeft, mSoftKeyDown.mTop, + mSoftKeyDown.mRight, mSoftKeyDown.mBottom); + invalidate(mDirtyRect); + } + + if (mSoftKeyDown.needBalloon()) { + mBalloonPopup.delayedDismiss(BalloonHint.TIME_DELAY_DISMISS); + } + + if (mSoftKeyDown.moveWithinKey(x - mPaddingLeft, y - mPaddingTop)) { + return mSoftKeyDown; + } + return null; + } + + public SoftKey onKeyMove(int x, int y) { + if (null == mSoftKeyDown) return null; + + if (mSoftKeyDown.moveWithinKey(x - mPaddingLeft, y - mPaddingTop)) { + return mSoftKeyDown; + } + + // The current key needs to be updated. + mDirtyRect.union(mSoftKeyDown.mLeft, mSoftKeyDown.mTop, + mSoftKeyDown.mRight, mSoftKeyDown.mBottom); + + if (mRepeatForLongPress) { + if (mMovingNeverHidePopupBalloon) { + return onKeyPress(x, y, mLongPressTimer, true); + } + + if (null != mBalloonOnKey) { + mBalloonOnKey.delayedDismiss(0); + } else { + invalidate(mDirtyRect); + } + + if (mSoftKeyDown.needBalloon()) { + mBalloonPopup.delayedDismiss(0); + } + + if (null != mLongPressTimer) { + mLongPressTimer.removeTimer(); + } + return onKeyPress(x, y, mLongPressTimer, true); + } else { + // When user moves between keys, repeated response is disabled. + return onKeyPress(x, y, mLongPressTimer, true); + } + } + + private void tryVibrate() { + if (!Settings.getVibrate()) { + return; + } + if (mVibrator == null) { + mVibrator = new Vibrator(); + } + mVibrator.vibrate(mVibratePattern, -1); + } + + private void tryPlayKeyDown() { + if (Settings.getKeySound()) { + mSoundManager.playKeyDown(); + } + } + + public void dimSoftKeyboard(boolean dimSkb) { + mDimSkb = dimSkb; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + if (null == mSoftKeyboard) return; + + canvas.translate(mPaddingLeft, mPaddingTop); + + Environment env = Environment.getInstance(); + mNormalKeyTextSize = env.getKeyTextSize(false); + mFunctionKeyTextSize = env.getKeyTextSize(true); + // Draw the last soft keyboard + int rowNum = mSoftKeyboard.getRowNum(); + int keyXMargin = mSoftKeyboard.getKeyXMargin(); + int keyYMargin = mSoftKeyboard.getKeyYMargin(); + for (int row = 0; row < rowNum; row++) { + KeyRow keyRow = mSoftKeyboard.getKeyRowForDisplay(row); + if (null == keyRow) continue; + List<SoftKey> softKeys = keyRow.mSoftKeys; + int keyNum = softKeys.size(); + for (int i = 0; i < keyNum; i++) { + SoftKey softKey = softKeys.get(i); + if (SoftKeyType.KEYTYPE_ID_NORMAL_KEY == softKey.mKeyType.mKeyTypeId) { + mPaint.setTextSize(mNormalKeyTextSize); + } else { + mPaint.setTextSize(mFunctionKeyTextSize); + } + drawSoftKey(canvas, softKey, keyXMargin, keyYMargin); + } + } + + if (mDimSkb) { + mPaint.setColor(0xa0000000); + canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint); + } + + mDirtyRect.setEmpty(); + } + + private void drawSoftKey(Canvas canvas, SoftKey softKey, int keyXMargin, + int keyYMargin) { + Drawable bg; + int textColor; + if (mKeyPressed && softKey == mSoftKeyDown) { + bg = softKey.getKeyHlBg(); + textColor = softKey.getColorHl(); + } else { + bg = softKey.getKeyBg(); + textColor = softKey.getColor(); + } + + if (null != bg) { + bg.setBounds(softKey.mLeft + keyXMargin, softKey.mTop + keyYMargin, + softKey.mRight - keyXMargin, softKey.mBottom - keyYMargin); + bg.draw(canvas); + } + + String keyLabel = softKey.getKeyLabel(); + Drawable keyIcon = softKey.getKeyIcon(); + if (null != keyIcon) { + Drawable icon = keyIcon; + int marginLeft = (softKey.width() - icon.getIntrinsicWidth()) / 2; + int marginRight = softKey.width() - icon.getIntrinsicWidth() + - marginLeft; + int marginTop = (softKey.height() - icon.getIntrinsicHeight()) / 2; + int marginBottom = softKey.height() - icon.getIntrinsicHeight() + - marginTop; + icon.setBounds(softKey.mLeft + marginLeft, + softKey.mTop + marginTop, softKey.mRight - marginRight, + softKey.mBottom - marginBottom); + icon.draw(canvas); + } else if (null != keyLabel) { + mPaint.setColor(textColor); + float x = softKey.mLeft + + (softKey.width() - mPaint.measureText(keyLabel)) / 2.0f; + int fontHeight = mFmi.bottom - mFmi.top; + float marginY = (softKey.height() - fontHeight) / 2.0f; + float y = softKey.mTop + marginY - mFmi.top + mFmi.bottom / 1.5f; + canvas.drawText(keyLabel, x, y + 1, mPaint); + } + } +} diff --git a/src/com/android/inputmethod/pinyin/SoundManager.java b/src/com/android/inputmethod/pinyin/SoundManager.java new file mode 100644 index 0000000..82be407 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/SoundManager.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import android.content.Context; +import android.media.AudioManager; + +/** + * Class used to manage related sound resources. + */ +public class SoundManager { + private static SoundManager mInstance = null; + private Context mContext; + private AudioManager mAudioManager; + private final float FX_VOLUME = 1.0f; + private boolean mSilentMode; + + private SoundManager(Context context) { + mContext = context; + updateRingerMode(); + } + + public void updateRingerMode() { + if (mAudioManager == null) { + mAudioManager = (AudioManager) mContext + .getSystemService(Context.AUDIO_SERVICE); + } + mSilentMode = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL); + } + + public static SoundManager getInstance(Context context) { + if (null == mInstance) { + if (null != context) { + mInstance = new SoundManager(context); + } + } + return mInstance; + } + + public void playKeyDown() { + if (mAudioManager == null) { + updateRingerMode(); + } + if (!mSilentMode) { + int sound = AudioManager.FX_KEYPRESS_STANDARD; + mAudioManager.playSoundEffect(sound, FX_VOLUME); + } + } +} diff --git a/src/com/android/inputmethod/pinyin/XmlKeyboardLoader.java b/src/com/android/inputmethod/pinyin/XmlKeyboardLoader.java new file mode 100644 index 0000000..fd192a3 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/XmlKeyboardLoader.java @@ -0,0 +1,835 @@ +/* + * Copyright (C) 2009 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.pinyin; + +import com.android.inputmethod.pinyin.SoftKeyboard.KeyRow; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; + +import java.io.IOException; +import java.util.regex.Pattern; + +import org.xmlpull.v1.XmlPullParserException; + +/** + * Class used to load a soft keyboard or a soft keyboard template from xml + * files. + */ +public class XmlKeyboardLoader { + /** + * The tag used to define an xml-based soft keyboard template. + */ + private static final String XMLTAG_SKB_TEMPLATE = "skb_template"; + + /** + * The tag used to indicate the soft key type which is defined inside the + * {@link #XMLTAG_SKB_TEMPLATE} element in the xml file. file. + */ + private static final String XMLTAG_KEYTYPE = "key_type"; + + /** + * The tag used to define a default key icon for enter/delete/space keys. It + * is defined inside the {@link #XMLTAG_SKB_TEMPLATE} element in the xml + * file. + */ + private static final String XMLTAG_KEYICON = "key_icon"; + + /** + * Attribute tag of the left and right margin for a key. A key's width + * should be larger than double of this value. Defined inside + * {@link #XMLTAG_SKB_TEMPLATE} and {@link #XMLTAG_KEYBOARD}. + */ + private static final String XMLATTR_KEY_XMARGIN = "key_xmargin"; + + /** + * Attribute tag of the top and bottom margin for a key. A key's height + * should be larger than double of this value. Defined inside + * {@link #XMLTAG_SKB_TEMPLATE} and {@link #XMLTAG_KEYBOARD}. + */ + private static final String XMLATTR_KEY_YMARGIN = "key_ymargin"; + + /** + * Attribute tag of the keyboard background image. Defined inside + * {@link #XMLTAG_SKB_TEMPLATE} and {@link #XMLTAG_KEYBOARD}. + */ + private static final String XMLATTR_SKB_BG = "skb_bg"; + + /** + * Attribute tag of the balloon background image for key press. Defined + * inside {@link #XMLTAG_SKB_TEMPLATE} and {@link #XMLTAG_KEYBOARD}. + */ + private static final String XMLATTR_BALLOON_BG = "balloon_bg"; + + /** + * Attribute tag of the popup balloon background image for key press or + * popup mini keyboard. Defined inside {@link #XMLTAG_SKB_TEMPLATE} and + * {@link #XMLTAG_KEYBOARD}. + */ + private static final String XMLATTR_POPUP_BG = "popup_bg"; + + /** + * Attribute tag of the color to draw key label. Defined inside + * {@link #XMLTAG_SKB_TEMPLATE} and {@link #XMLTAG_KEYTYPE}. + */ + private static final String XMLATTR_COLOR = "color"; + + /** + * Attribute tag of the color to draw key's highlighted label. Defined + * inside {@link #XMLTAG_SKB_TEMPLATE} and {@link #XMLTAG_KEYTYPE}. + */ + private static final String XMLATTR_COLOR_HIGHLIGHT = "color_highlight"; + + /** + * Attribute tag of the color to draw key's label in the popup balloon. + * Defined inside {@link #XMLTAG_SKB_TEMPLATE} and {@link #XMLTAG_KEYTYPE}. + */ + private static final String XMLATTR_COLOR_BALLOON = "color_balloon"; + + /** + * Attribute tag of the id of {@link #XMLTAG_KEYTYPE} and + * {@link #XMLTAG_KEY}. Key types and keys defined in a soft keyboard + * template should have id, because a soft keyboard needs the id to refer to + * these default definitions. If a key defined in {@link #XMLTAG_KEYBOARD} + * does not id, that means the key is newly defined; if it has id (and only + * has id), the id is used to find the default definition from the soft + * keyboard template. + */ + private static final String XMLATTR_ID = "id"; + + /** + * Attribute tag of the key background for a specified key type. Defined + * inside {@link #XMLTAG_KEYTYPE}. + */ + private static final String XMLATTR_KEYTYPE_BG = "bg"; + + /** + * Attribute tag of the key high-light background for a specified key type. + * Defined inside {@link #XMLTAG_KEYTYPE}. + */ + private static final String XMLATTR_KEYTYPE_HLBG = "hlbg"; + + /** + * Attribute tag of the starting x-position of an element. It can be defined + * in {@link #XMLTAG_ROW} and {@link #XMLTAG_KEY} in {XMLTAG_SKB_TEMPLATE}. + * If not defined, 0 will be used. For a key defined in + * {@link #XMLTAG_KEYBOARD}, it always use its previous keys information to + * calculate its own position. + */ + private static final String XMLATTR_START_POS_X = "start_pos_x"; + + /** + * Attribute tag of the starting y-position of an element. It can be defined + * in {@link #XMLTAG_ROW} and {@link #XMLTAG_KEY} in {XMLTAG_SKB_TEMPLATE}. + * If not defined, 0 will be used. For a key defined in + * {@link #XMLTAG_KEYBOARD}, it always use its previous keys information to + * calculate its own position. + */ + private static final String XMLATTR_START_POS_Y = "start_pos_y"; + + /** + * Attribute tag of a row's id. Defined {@link #XMLTAG_ROW}. If not defined, + * -1 will be used. Rows with id -1 will be enabled always, rows with same + * row id will be enabled when the id is the same to the activated id of the + * soft keyboard. + */ + private static final String XMLATTR_ROW_ID = "row_id"; + + /** The tag used to indicate the keyboard element in the xml file. */ + private static final String XMLTAG_KEYBOARD = "keyboard"; + + /** The tag used to indicate the row element in the xml file. */ + private static final String XMLTAG_ROW = "row"; + + /** The tag used to indicate key-array element in the xml file. */ + private static final String XMLTAG_KEYS = "keys"; + + /** + * The tag used to indicate a key element in the xml file. If the element is + * defined in a soft keyboard template, it should have an id. If it is + * defined in a soft keyboard, id is not required. + */ + private static final String XMLTAG_KEY = "key"; + + /** The tag used to indicate a key's toggle element in the xml file. */ + private static final String XMLTAG_TOGGLE_STATE = "toggle_state"; + + /** + * Attribute tag of the toggle state id for toggle key. Defined inside + * {@link #XMLTAG_TOGGLE_STATE} + */ + private static final String XMLATTR_TOGGLE_STATE_ID = "state_id"; + + /** Attribute tag of key template for the soft keyboard. */ + private static final String XMLATTR_SKB_TEMPLATE = "skb_template"; + + /** + * Attribute tag used to indicate whether this soft keyboard needs to be + * cached in memory for future use. {@link #DEFAULT_SKB_CACHE_FLAG} + * specifies the default value. + */ + private static final String XMLATTR_SKB_CACHE_FLAG = "skb_cache_flag"; + + /** + * Attribute tag used to indicate whether this soft keyboard is sticky. A + * sticky soft keyboard will keep the current layout unless user makes a + * switch explicitly. A none sticky soft keyboard will automatically goes + * back to the previous keyboard after click a none-function key. + * {@link #DEFAULT_SKB_STICKY_FLAG} specifies the default value. + */ + private static final String XMLATTR_SKB_STICKY_FLAG = "skb_sticky_flag"; + + /** Attribute tag to indicate whether it is a QWERTY soft keyboard. */ + private static final String XMLATTR_QWERTY = "qwerty"; + + /** + * When the soft keyboard is a QWERTY one, this attribute tag to get the + * information that whether it is defined in upper case. + */ + private static final String XMLATTR_QWERTY_UPPERCASE = "qwerty_uppercase"; + + /** Attribute tag of key type. */ + private static final String XMLATTR_KEY_TYPE = "key_type"; + + /** Attribute tag of key width. */ + private static final String XMLATTR_KEY_WIDTH = "width"; + + /** Attribute tag of key height. */ + private static final String XMLATTR_KEY_HEIGHT = "height"; + + /** Attribute tag of the key's repeating ability. */ + private static final String XMLATTR_KEY_REPEAT = "repeat"; + + /** Attribute tag of the key's behavior for balloon. */ + private static final String XMLATTR_KEY_BALLOON = "balloon"; + + /** Attribute tag of the key splitter in a key array. */ + private static final String XMLATTR_KEY_SPLITTER = "splitter"; + + /** Attribute tag of the key labels in a key array. */ + private static final String XMLATTR_KEY_LABELS = "labels"; + + /** Attribute tag of the key codes in a key array. */ + private static final String XMLATTR_KEY_CODES = "codes"; + + /** Attribute tag of the key label in a key. */ + private static final String XMLATTR_KEY_LABEL = "label"; + + /** Attribute tag of the key code in a key. */ + private static final String XMLATTR_KEY_CODE = "code"; + + /** Attribute tag of the key icon in a key. */ + private static final String XMLATTR_KEY_ICON = "icon"; + + /** Attribute tag of the key's popup icon in a key. */ + private static final String XMLATTR_KEY_ICON_POPUP = "icon_popup"; + + /** The id for a mini popup soft keyboard. */ + private static final String XMLATTR_KEY_POPUP_SKBID = "popup_skb"; + + private static boolean DEFAULT_SKB_CACHE_FLAG = true; + + private static boolean DEFAULT_SKB_STICKY_FLAG = true; + + /** + * The key type id for invalid key type. It is also used to generate next + * valid key type id by adding 1. + */ + private static final int KEYTYPE_ID_LAST = -1; + + private Context mContext; + + private Resources mResources; + + /** The event type in parsing the xml file. */ + private int mXmlEventType; + + /** + * The current soft keyboard template used by the current soft keyboard + * under loading. + **/ + private SkbTemplate mSkbTemplate; + + /** The x position for the next key. */ + float mKeyXPos; + + /** The y position for the next key. */ + float mKeyYPos; + + /** The width of the keyboard to load. */ + int mSkbWidth; + + /** The height of the keyboard to load. */ + int mSkbHeight; + + /** Key margin in x-way. */ + float mKeyXMargin = 0; + + /** Key margin in y-way. */ + float mKeyYMargin = 0; + + /** + * Used to indicate whether next event has been fetched during processing + * the the current event. + */ + boolean mNextEventFetched = false; + + String mAttrTmp; + + class KeyCommonAttributes { + XmlResourceParser mXrp; + int keyType; + float keyWidth; + float keyHeight; + boolean repeat; + boolean balloon; + + KeyCommonAttributes(XmlResourceParser xrp) { + mXrp = xrp; + balloon = true; + } + + // Make sure the default object is not null. + boolean getAttributes(KeyCommonAttributes defAttr) { + keyType = getInteger(mXrp, XMLATTR_KEY_TYPE, defAttr.keyType); + keyWidth = getFloat(mXrp, XMLATTR_KEY_WIDTH, defAttr.keyWidth); + keyHeight = getFloat(mXrp, XMLATTR_KEY_HEIGHT, defAttr.keyHeight); + repeat = getBoolean(mXrp, XMLATTR_KEY_REPEAT, defAttr.repeat); + balloon = getBoolean(mXrp, XMLATTR_KEY_BALLOON, defAttr.balloon); + if (keyType < 0 || keyWidth <= 0 || keyHeight <= 0) { + return false; + } + return true; + } + } + + public XmlKeyboardLoader(Context context) { + mContext = context; + mResources = mContext.getResources(); + } + + public SkbTemplate loadSkbTemplate(int resourceId) { + if (null == mContext || 0 == resourceId) { + return null; + } + Resources r = mResources; + XmlResourceParser xrp = r.getXml(resourceId); + + KeyCommonAttributes attrDef = new KeyCommonAttributes(xrp); + KeyCommonAttributes attrKey = new KeyCommonAttributes(xrp); + + mSkbTemplate = new SkbTemplate(resourceId); + int lastKeyTypeId = KEYTYPE_ID_LAST; + int globalColor = 0; + int globalColorHl = 0; + int globalColorBalloon = 0; + try { + mXmlEventType = xrp.next(); + while (mXmlEventType != XmlResourceParser.END_DOCUMENT) { + mNextEventFetched = false; + if (mXmlEventType == XmlResourceParser.START_TAG) { + String attribute = xrp.getName(); + if (XMLTAG_SKB_TEMPLATE.compareTo(attribute) == 0) { + Drawable skbBg = getDrawable(xrp, XMLATTR_SKB_BG, null); + Drawable balloonBg = getDrawable(xrp, + XMLATTR_BALLOON_BG, null); + Drawable popupBg = getDrawable(xrp, XMLATTR_POPUP_BG, + null); + if (null == skbBg || null == balloonBg + || null == popupBg) { + return null; + } + mSkbTemplate.setBackgrounds(skbBg, balloonBg, popupBg); + + float xMargin = getFloat(xrp, XMLATTR_KEY_XMARGIN, 0); + float yMargin = getFloat(xrp, XMLATTR_KEY_YMARGIN, 0); + mSkbTemplate.setMargins(xMargin, yMargin); + + // Get default global colors. + globalColor = getColor(xrp, XMLATTR_COLOR, 0); + globalColorHl = getColor(xrp, XMLATTR_COLOR_HIGHLIGHT, + 0xffffffff); + globalColorBalloon = getColor(xrp, + XMLATTR_COLOR_BALLOON, 0xffffffff); + } else if (XMLTAG_KEYTYPE.compareTo(attribute) == 0) { + int id = getInteger(xrp, XMLATTR_ID, KEYTYPE_ID_LAST); + Drawable bg = getDrawable(xrp, XMLATTR_KEYTYPE_BG, null); + Drawable hlBg = getDrawable(xrp, XMLATTR_KEYTYPE_HLBG, + null); + int color = getColor(xrp, XMLATTR_COLOR, globalColor); + int colorHl = getColor(xrp, XMLATTR_COLOR_HIGHLIGHT, + globalColorHl); + int colorBalloon = getColor(xrp, XMLATTR_COLOR_BALLOON, + globalColorBalloon); + if (id != lastKeyTypeId + 1) { + return null; + } + SoftKeyType keyType = mSkbTemplate.createKeyType(id, + bg, hlBg); + keyType.setColors(color, colorHl, colorBalloon); + if (!mSkbTemplate.addKeyType(keyType)) { + return null; + } + lastKeyTypeId = id; + } else if (XMLTAG_KEYICON.compareTo(attribute) == 0) { + int keyCode = getInteger(xrp, XMLATTR_KEY_CODE, 0); + Drawable icon = getDrawable(xrp, XMLATTR_KEY_ICON, null); + Drawable iconPopup = getDrawable(xrp, + XMLATTR_KEY_ICON_POPUP, null); + if (null != icon && null != iconPopup) { + mSkbTemplate.addDefaultKeyIcons(keyCode, icon, + iconPopup); + } + } else if (XMLTAG_KEY.compareTo(attribute) == 0) { + int keyId = this.getInteger(xrp, XMLATTR_ID, -1); + if (-1 == keyId) return null; + + if (!attrKey.getAttributes(attrDef)) { + return null; + } + + // Update the key position for the key. + mKeyXPos = getFloat(xrp, XMLATTR_START_POS_X, 0); + mKeyYPos = getFloat(xrp, XMLATTR_START_POS_Y, 0); + + SoftKey softKey = getSoftKey(xrp, attrKey); + if (null == softKey) return null; + mSkbTemplate.addDefaultKey(keyId, softKey); + } + } + // Get the next tag. + if (!mNextEventFetched) mXmlEventType = xrp.next(); + } + xrp.close(); + return mSkbTemplate; + } catch (XmlPullParserException e) { + // Log.e(TAG, "Ill-formatted keyboard template resource file"); + } catch (IOException e) { + // Log.e(TAG, "Unable to keyboard template resource file"); + } + return null; + } + + public SoftKeyboard loadKeyboard(int resourceId, int skbWidth, int skbHeight) { + if (null == mContext) return null; + Resources r = mResources; + SkbPool skbPool = SkbPool.getInstance(); + XmlResourceParser xrp = mContext.getResources().getXml(resourceId); + mSkbTemplate = null; + SoftKeyboard softKeyboard = null; + Drawable skbBg; + Drawable popupBg; + Drawable balloonBg; + SoftKey softKey = null; + + KeyCommonAttributes attrDef = new KeyCommonAttributes(xrp); + KeyCommonAttributes attrSkb = new KeyCommonAttributes(xrp); + KeyCommonAttributes attrRow = new KeyCommonAttributes(xrp); + KeyCommonAttributes attrKeys = new KeyCommonAttributes(xrp); + KeyCommonAttributes attrKey = new KeyCommonAttributes(xrp); + + mKeyXPos = 0; + mKeyYPos = 0; + mSkbWidth = skbWidth; + mSkbHeight = skbHeight; + + try { + mKeyXMargin = 0; + mKeyYMargin = 0; + mXmlEventType = xrp.next(); + while (mXmlEventType != XmlResourceParser.END_DOCUMENT) { + mNextEventFetched = false; + if (mXmlEventType == XmlResourceParser.START_TAG) { + String attr = xrp.getName(); + // 1. Is it the root element, "keyboard"? + if (XMLTAG_KEYBOARD.compareTo(attr) == 0) { + // 1.1 Get the keyboard template id. + int skbTemplateId = xrp.getAttributeResourceValue(null, + XMLATTR_SKB_TEMPLATE, 0); + + // 1.2 Try to get the template from pool. If it is not + // in, the pool will try to load it. + mSkbTemplate = skbPool.getSkbTemplate(skbTemplateId, + mContext); + + if (null == mSkbTemplate + || !attrSkb.getAttributes(attrDef)) { + return null; + } + + boolean cacheFlag = getBoolean(xrp, + XMLATTR_SKB_CACHE_FLAG, DEFAULT_SKB_CACHE_FLAG); + boolean stickyFlag = getBoolean(xrp, + XMLATTR_SKB_STICKY_FLAG, + DEFAULT_SKB_STICKY_FLAG); + boolean isQwerty = getBoolean(xrp, XMLATTR_QWERTY, + false); + boolean isQwertyUpperCase = getBoolean(xrp, + XMLATTR_QWERTY_UPPERCASE, false); + + softKeyboard = new SoftKeyboard(resourceId, + mSkbTemplate, mSkbWidth, mSkbHeight); + softKeyboard.setFlags(cacheFlag, stickyFlag, isQwerty, + isQwertyUpperCase); + + mKeyXMargin = getFloat(xrp, XMLATTR_KEY_XMARGIN, + mSkbTemplate.getXMargin()); + mKeyYMargin = getFloat(xrp, XMLATTR_KEY_YMARGIN, + mSkbTemplate.getYMargin()); + skbBg = getDrawable(xrp, XMLATTR_SKB_BG, null); + popupBg = getDrawable(xrp, XMLATTR_POPUP_BG, null); + balloonBg = getDrawable(xrp, XMLATTR_BALLOON_BG, null); + if (null != skbBg) { + softKeyboard.setSkbBackground(skbBg); + } + if (null != popupBg) { + softKeyboard.setPopupBackground(popupBg); + } + if (null != balloonBg) { + softKeyboard.setKeyBalloonBackground(balloonBg); + } + softKeyboard.setKeyMargins(mKeyXMargin, mKeyYMargin); + } else if (XMLTAG_ROW.compareTo(attr) == 0) { + if (!attrRow.getAttributes(attrSkb)) { + return null; + } + // Get the starting positions for the row. + mKeyXPos = getFloat(xrp, XMLATTR_START_POS_X, 0); + mKeyYPos = getFloat(xrp, XMLATTR_START_POS_Y, mKeyYPos); + int rowId = getInteger(xrp, XMLATTR_ROW_ID, + KeyRow.ALWAYS_SHOW_ROW_ID); + softKeyboard.beginNewRow(rowId, mKeyYPos); + } else if (XMLTAG_KEYS.compareTo(attr) == 0) { + if (null == softKeyboard) return null; + if (!attrKeys.getAttributes(attrRow)) { + return null; + } + + String splitter = xrp.getAttributeValue(null, + XMLATTR_KEY_SPLITTER); + splitter = Pattern.quote(splitter); + String labels = xrp.getAttributeValue(null, + XMLATTR_KEY_LABELS); + String codes = xrp.getAttributeValue(null, + XMLATTR_KEY_CODES); + if (null == splitter || null == labels) { + return null; + } + String labelArr[] = labels.split(splitter); + String codeArr[] = null; + if (null != codes) { + codeArr = codes.split(splitter); + if (labelArr.length != codeArr.length) { + return null; + } + } + + for (int i = 0; i < labelArr.length; i++) { + softKey = new SoftKey(); + int keyCode = 0; + if (null != codeArr) { + keyCode = Integer.valueOf(codeArr[i]); + } + softKey.setKeyAttribute(keyCode, labelArr[i], + attrKeys.repeat, attrKeys.balloon); + + softKey.setKeyType(mSkbTemplate + .getKeyType(attrKeys.keyType), null, null); + + float left, right, top, bottom; + left = mKeyXPos; + + right = left + attrKeys.keyWidth; + top = mKeyYPos; + bottom = top + attrKeys.keyHeight; + + if (right - left < 2 * mKeyXMargin) return null; + if (bottom - top < 2 * mKeyYMargin) return null; + + softKey.setKeyDimensions(left, top, right, bottom); + softKeyboard.addSoftKey(softKey); + mKeyXPos = right; + if ((int) mKeyXPos * mSkbWidth > mSkbWidth) { + return null; + } + } + } else if (XMLTAG_KEY.compareTo(attr) == 0) { + if (null == softKeyboard) { + return null; + } + if (!attrKey.getAttributes(attrRow)) { + return null; + } + + int keyId = this.getInteger(xrp, XMLATTR_ID, -1); + if (keyId >= 0) { + softKey = mSkbTemplate.getDefaultKey(keyId); + } else { + softKey = getSoftKey(xrp, attrKey); + } + if (null == softKey) return null; + + // Update the position for next key. + mKeyXPos = softKey.mRightF; + if ((int) mKeyXPos * mSkbWidth > mSkbWidth) { + return null; + } + // If the current xml event type becomes a starting tag, + // it indicates that we have parsed too much to get + // toggling states, and we started a new row. In this + // case, the row starting position information should + // be updated. + if (mXmlEventType == XmlResourceParser.START_TAG) { + attr = xrp.getName(); + if (XMLTAG_ROW.compareTo(attr) == 0) { + mKeyYPos += attrRow.keyHeight; + if ((int) mKeyYPos * mSkbHeight > mSkbHeight) { + return null; + } + } + } + softKeyboard.addSoftKey(softKey); + } + } else if (mXmlEventType == XmlResourceParser.END_TAG) { + String attr = xrp.getName(); + if (XMLTAG_ROW.compareTo(attr) == 0) { + mKeyYPos += attrRow.keyHeight; + if ((int) mKeyYPos * mSkbHeight > mSkbHeight) { + return null; + } + } + } + + // Get the next tag. + if (!mNextEventFetched) mXmlEventType = xrp.next(); + } + xrp.close(); + softKeyboard.setSkbCoreSize(mSkbWidth, mSkbHeight); + return softKeyboard; + } catch (XmlPullParserException e) { + // Log.e(TAG, "Ill-formatted keybaord resource file"); + } catch (IOException e) { + // Log.e(TAG, "Unable to read keyboard resource file"); + } + return null; + } + + // Caller makes sure xrp and r are valid. + private SoftKey getSoftKey(XmlResourceParser xrp, + KeyCommonAttributes attrKey) throws XmlPullParserException, + IOException { + int keyCode = getInteger(xrp, XMLATTR_KEY_CODE, 0); + String keyLabel = getString(xrp, XMLATTR_KEY_LABEL, null); + Drawable keyIcon = getDrawable(xrp, XMLATTR_KEY_ICON, null); + Drawable keyIconPopup = getDrawable(xrp, XMLATTR_KEY_ICON_POPUP, null); + int popupSkbId = xrp.getAttributeResourceValue(null, + XMLATTR_KEY_POPUP_SKBID, 0); + + if (null == keyLabel && null == keyIcon) { + keyIcon = mSkbTemplate.getDefaultKeyIcon(keyCode); + keyIconPopup = mSkbTemplate.getDefaultKeyIconPopup(keyCode); + if (null == keyIcon || null == keyIconPopup) return null; + } + + // Dimension information must been initialized before + // getting toggle state, because mKeyYPos may be changed + // to next row when trying to get toggle state. + float left, right, top, bottom; + left = mKeyXPos; + right = left + attrKey.keyWidth; + top = mKeyYPos; + bottom = top + attrKey.keyHeight; + + if (right - left < 2 * mKeyXMargin) return null; + if (bottom - top < 2 * mKeyYMargin) return null; + + // Try to find if the next tag is + // {@link #XMLTAG_TOGGLE_STATE_OF_KEY}, if yes, try to + // create a toggle key. + boolean toggleKey = false; + mXmlEventType = xrp.next(); + mNextEventFetched = true; + + SoftKey softKey; + if (mXmlEventType == XmlResourceParser.START_TAG) { + mAttrTmp = xrp.getName(); + if (mAttrTmp.compareTo(XMLTAG_TOGGLE_STATE) == 0) { + toggleKey = true; + } + } + if (toggleKey) { + softKey = new SoftKeyToggle(); + if (!((SoftKeyToggle) softKey).setToggleStates(getToggleStates( + attrKey, (SoftKeyToggle) softKey, keyCode))) { + return null; + } + } else { + softKey = new SoftKey(); + } + + // Set the normal state + softKey.setKeyAttribute(keyCode, keyLabel, attrKey.repeat, + attrKey.balloon); + softKey.setPopupSkbId(popupSkbId); + softKey.setKeyType(mSkbTemplate.getKeyType(attrKey.keyType), keyIcon, + keyIconPopup); + + softKey.setKeyDimensions(left, top, right, bottom); + return softKey; + } + + private SoftKeyToggle.ToggleState getToggleStates( + KeyCommonAttributes attrKey, SoftKeyToggle softKey, int defKeyCode) + throws XmlPullParserException, IOException { + XmlResourceParser xrp = attrKey.mXrp; + int stateId = getInteger(xrp, XMLATTR_TOGGLE_STATE_ID, 0); + if (0 == stateId) return null; + + String keyLabel = getString(xrp, XMLATTR_KEY_LABEL, null); + int keyTypeId = getInteger(xrp, XMLATTR_KEY_TYPE, KEYTYPE_ID_LAST); + int keyCode; + if (null == keyLabel) { + keyCode = getInteger(xrp, XMLATTR_KEY_CODE, defKeyCode); + } else { + keyCode = getInteger(xrp, XMLATTR_KEY_CODE, 0); + } + Drawable icon = getDrawable(xrp, XMLATTR_KEY_ICON, null); + Drawable iconPopup = getDrawable(xrp, XMLATTR_KEY_ICON_POPUP, null); + if (null == icon && null == keyLabel) { + return null; + } + SoftKeyToggle.ToggleState rootState = softKey.createToggleState(); + rootState.setStateId(stateId); + rootState.mKeyType = null; + if (KEYTYPE_ID_LAST != keyTypeId) { + rootState.mKeyType = mSkbTemplate.getKeyType(keyTypeId); + } + rootState.mKeyCode = keyCode; + rootState.mKeyIcon = icon; + rootState.mKeyIconPopup = iconPopup; + rootState.mKeyLabel = keyLabel; + + boolean repeat = getBoolean(xrp, XMLATTR_KEY_REPEAT, attrKey.repeat); + boolean balloon = getBoolean(xrp, XMLATTR_KEY_BALLOON, attrKey.balloon); + rootState.setStateFlags(repeat, balloon); + + rootState.mNextState = null; + + // If there is another toggle state. + mXmlEventType = xrp.next(); + while (mXmlEventType != XmlResourceParser.START_TAG + && mXmlEventType != XmlResourceParser.END_DOCUMENT) { + mXmlEventType = xrp.next(); + } + if (mXmlEventType == XmlResourceParser.START_TAG) { + String attr = xrp.getName(); + if (attr.compareTo(XMLTAG_TOGGLE_STATE) == 0) { + SoftKeyToggle.ToggleState nextState = getToggleStates(attrKey, + softKey, defKeyCode); + if (null == nextState) return null; + rootState.mNextState = nextState; + } + } + + return rootState; + } + + private int getInteger(XmlResourceParser xrp, String name, int defValue) { + int resId = xrp.getAttributeResourceValue(null, name, 0); + String s; + if (resId == 0) { + s = xrp.getAttributeValue(null, name); + if (null == s) return defValue; + try { + int ret = Integer.valueOf(s); + return ret; + } catch (NumberFormatException e) { + return defValue; + } + } else { + return Integer.parseInt(mContext.getResources().getString(resId)); + } + } + + private int getColor(XmlResourceParser xrp, String name, int defValue) { + int resId = xrp.getAttributeResourceValue(null, name, 0); + String s; + if (resId == 0) { + s = xrp.getAttributeValue(null, name); + if (null == s) return defValue; + try { + int ret = Integer.valueOf(s); + return ret; + } catch (NumberFormatException e) { + return defValue; + } + } else { + return mContext.getResources().getColor(resId); + } + } + + private String getString(XmlResourceParser xrp, String name, String defValue) { + int resId = xrp.getAttributeResourceValue(null, name, 0); + if (resId == 0) { + return xrp.getAttributeValue(null, name); + } else { + return mContext.getResources().getString(resId); + } + } + + private float getFloat(XmlResourceParser xrp, String name, float defValue) { + int resId = xrp.getAttributeResourceValue(null, name, 0); + if (resId == 0) { + String s = xrp.getAttributeValue(null, name); + if (null == s) return defValue; + try { + float ret; + if (s.endsWith("%p")) { + ret = Float.parseFloat(s.substring(0, s.length() - 2)) / 100; + } else { + ret = Float.parseFloat(s); + } + return ret; + } catch (NumberFormatException e) { + return defValue; + } + } else { + return mContext.getResources().getDimension(resId); + } + } + + private boolean getBoolean(XmlResourceParser xrp, String name, + boolean defValue) { + String s = xrp.getAttributeValue(null, name); + if (null == s) return defValue; + try { + boolean ret = Boolean.parseBoolean(s); + return ret; + } catch (NumberFormatException e) { + return defValue; + } + } + + private Drawable getDrawable(XmlResourceParser xrp, String name, + Drawable defValue) { + int resId = xrp.getAttributeResourceValue(null, name, 0); + if (0 == resId) return defValue; + return mResources.getDrawable(resId); + } +} |