/*
** Copyright 2010, The LimeIME Open Source Project
**
** Project Url: http://code.google.com/p/limeime/
** http://android.toload.net/
**
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
** You should have received a copy of the GNU General Public License
** along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.toload.main.hd.candidate;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Display;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.ScrollView;
import android.widget.TextView;
import java.util.LinkedList;
import java.util.List;
import java.lang.Math;
import net.toload.main.hd.LIMEService;
import net.toload.main.hd.R;
import net.toload.main.hd.global.LIMEPreferenceManager;
import net.toload.main.hd.global.Mapping;
/**
* @author Art Hung
*/
public class CandidateView extends View implements View.OnClickListener
{
private static final boolean DEBUG = false;
private static final String TAG = "CandidateView";
protected static final int OUT_OF_BOUNDS = -1;
protected LIMEService mService;
protected List<Mapping> mSuggestions;
protected int mSelectedIndex;
protected int mTouchX = OUT_OF_BOUNDS;
protected Drawable mSelectionHighlight;
//private boolean mTypedWordValid;
private boolean mShowNumber; //Jeremy '11,5,25 for showing physical keyboard number or not.
protected Rect mBgPadding;
protected Rect cursorRect=null; //Jeremy '11,7,25 for store current cursor rect
private static final int MAX_SUGGESTIONS = 500;
private static final int SCROLL_PIXELS = 20;
// Add by Jeremy '10, 3, 29.
// Suggestions size. Set to MAX_GUGGESTIONS if larger then it.
protected int mCount =0 ;
//Composing view
private TextView mComposingTextView;
private PopupWindow mComposingTextPopup;
//private String mComposingText = "";
protected int[] mWordWidth = new int[MAX_SUGGESTIONS];
protected int[] mWordX = new int[MAX_SUGGESTIONS];
protected static int X_GAP = 12;
private static final List<Mapping> EMPTY_LIST = new LinkedList<Mapping>();
protected int mHeight;
private int currentX;
protected final int mColorNormal;
protected final int mColorInverted;
protected final int mColorDictionary;
protected final int mColorRecommended;
protected final int mColorOther;
protected final int mColorNumber;
protected int mVerticalPadding;
protected int mExpandButtonWidth;
protected Paint mPaint;
protected Paint nPaint;
//private Paint cPaint;
private boolean mScrolled;
protected int mTargetScrollX;
private String mDisplaySelkey = "1234567890";
protected int mTotalWidth;
private boolean goLeft = false;
private boolean goRight = false;
private boolean hasSlide = false;
//private int bgcolor = 0;
private View mCandidatePopupContainer;
private PopupWindow mCandidatePopupWindow;
protected int mScreenWidth;
protected int mScreenHeight;
protected GestureDetector mGestureDetector;
protected final Context mContext;
private final boolean isAndroid3; //'11,8,11, Jeremy
protected LIMEPreferenceManager mLIMEPref;
private CandidateExpandedView mPopupCandidateView;
private int mCloseButtonHeight;
private ScrollView mPopupScrollView;
private boolean candidateExpanded = false;
private boolean waitingForMoreRecords = false;
//private Rect padding = null;
/**
* Construct a CandidateView for showing suggested words for completion.
* @param context
* @param attrs
*/
@TargetApi(13)
@SuppressWarnings("deprecation")
public CandidateView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
//Jeremy '11,8,11 detect API level and show keyboard number only in 3.0+
isAndroid3 = android.os.Build.VERSION.SDK_INT >11;
mLIMEPref = new LIMEPreferenceManager(context);
Resources r = context.getResources();
Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
if(android.os.Build.VERSION.SDK_INT < 13) {
mScreenWidth = display.getWidth();
mScreenHeight = display.getHeight();
}else{
Point screenSize = new Point();
display.getSize(screenSize);
mScreenWidth = screenSize.x;
mScreenHeight = screenSize.y;
}
mSelectionHighlight = r.getDrawable(
R.drawable.list_selector_background);
//android.R.drawable.list_selector_background);
mSelectionHighlight.setState(new int[] {
android.R.attr.state_enabled,
android.R.attr.state_focused,
android.R.attr.state_window_focused,
android.R.attr.state_pressed
});
//bgcolor = r.getColor(R.color.candidate_background);
mColorNormal = r.getColor(R.color.candidate_normal);
mColorInverted = r.getColor(R.color.candidate_inverted);
mColorDictionary = r.getColor(R.color.candidate_dictionary);
mColorRecommended = r.getColor(R.color.candidate_recommended);
mColorOther = r.getColor(R.color.candidate_other);
mColorNumber = r.getColor(R.color.candidate_number);
mVerticalPadding =(int)( r.getDimensionPixelSize(R.dimen.candidate_vertical_padding)*mLIMEPref.getFontSize());
mHeight = (int) (r.getDimensionPixelSize(R.dimen.candidate_stripe_height) *mLIMEPref.getFontSize());
mExpandButtonWidth = r.getDimensionPixelSize(R.dimen.candidate_expand_button_width);// *mLIMEPref.getFontSize());
mPaint = new Paint();
mPaint.setColor(mColorNormal);
mPaint.setAntiAlias(true);
mPaint.setTextSize(r.getDimensionPixelSize(R.dimen.candidate_font_size)*mLIMEPref.getFontSize());
mPaint.setStrokeWidth(0);
nPaint = new Paint();
nPaint.setColor(mColorNumber);
nPaint.setAntiAlias(true);
nPaint.setTextSize(r.getDimensionPixelSize(R.dimen.candidate_number_font_size)*mLIMEPref.getFontSize());
nPaint.setStyle(Paint.Style.FILL_AND_STROKE);
//final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
//Jeremy '12,4,23 add mContext parameter. The construstor without context is deprecated
mGestureDetector = new GestureDetector(mContext, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
if(DEBUG)
Log.i(TAG, "onScroll(): distanceX = " + distanceX + "; distanceY = " + distanceY );
//Jeremy '12,4,8 filter out small scroll which is actually candidate selection.
if(Math.abs(distanceX) < mHeight/5 && Math.abs(distanceY) < mHeight/5 ) return true;
mScrolled = true;
// Update full candidate list before scroll
checkHasMoreRecords();
int sx = getScrollX();
sx += distanceX;
if (sx < 0) {
sx = 0;
}
if (sx + getWidth() > mTotalWidth) {
sx -= distanceX;
}
if(mLIMEPref.getParameterBoolean("candidate_switch", false)){
hasSlide = true;
mTargetScrollX = sx;
scrollTo(sx, getScrollY());
currentX = getScrollX(); //Jeremy '12,7,6 set currentX to the left edge of current scrollview after scrolled
}else{
hasSlide = false;
if(distanceX < 0){
goLeft = true;
goRight = false;
}else if(distanceX > 0){
goLeft = false;
goRight = true;
}else{
mTargetScrollX = sx;
}
}
//invalidate();
return true;
}
});
/*setHorizontalFadingEdgeEnabled(true);
setWillNotDraw(false);
setHorizontalScrollBarEnabled(false);
setVerticalScrollBarEnabled(false);*/
}
private final UIHandler mHandler = new UIHandler();
class UIHandler extends Handler {
private static final int MSG_UPDATE_UI = 1;
private static final int MSG_UPDATE_COMPOSING = 2;
private static final int MSG_HIDE_COMPOSING = 3;
private static final int MSG_SHOW_CANDIDATE_POPUP = 4;
private static final int MSG_HIDE_CANDIDATE_POPUP = 5;
private static final int MSG_SET_COMPOSING = 6;
@Override
public void handleMessage(Message msg) {
if(DEBUG) Log.i(TAG,"UIHandler.handlMessage(): message:" + msg.what);
switch (msg.what) {
case MSG_UPDATE_UI:
doUpdateUI();
break;
case MSG_UPDATE_COMPOSING:
doUpdateComposing();
break;
case MSG_HIDE_COMPOSING: {
if(mComposingTextPopup!=null && mComposingTextPopup.isShowing()) {
mComposingTextPopup.dismiss();
}
break;
}
case MSG_SHOW_CANDIDATE_POPUP: {
doUpdateCandidatePopup();
break;
}
case MSG_HIDE_CANDIDATE_POPUP: {
doHideCandidatePopup();
break;
}
case MSG_SET_COMPOSING: {
String composingText = (String) msg.obj;
if(DEBUG) Log.i(TAG, "UIHandler.handleMessage(): compsoingText" + composingText);
doSetComposing(composingText);
break;
}
}
}
public void updateUI (int delay){
sendMessageDelayed(obtainMessage(MSG_UPDATE_UI, 0, 0, null), delay);
}
public void setComposing (String text ,int delay){
sendMessageDelayed(obtainMessage(MSG_SET_COMPOSING, 0, 0, text), delay);
}
public void updateComposing (int delay){
sendMessageDelayed(obtainMessage(MSG_UPDATE_COMPOSING, 0, 0, null), delay);
}
public void dismissComposing(int delay) {
sendMessageDelayed(obtainMessage(MSG_HIDE_COMPOSING, 0, 0, null), delay);
}
public void showCandidatePopup(int delay) {
sendMessageDelayed(obtainMessage(MSG_SHOW_CANDIDATE_POPUP, 0, 0, null), delay);
}
public void dismissCandidatePopup(int delay) {
sendMessageDelayed(obtainMessage(MSG_HIDE_CANDIDATE_POPUP, 0, 0, null), delay);
}
}
public void doUpdateUI() {
if(DEBUG)
Log.i(TAG,"doUpdateUI()");
if (mSuggestions == null) {
//setBackgroundColor(0);
hideCandidatePopup();
return;
}
//setBackgroundColor(bgcolor);
if(mCandidatePopupWindow != null && mCandidatePopupWindow.isShowing()){
//doHideCandidatePopup();
doUpdateCandidatePopup();
}else{
if(!waitingForMoreRecords){ // New suggestion list, reset scroll to (0,0);
scrollTo(0, 0);
mTargetScrollX = 0;
}
onDraw(null);
resetWidth();
invalidate();
requestLayout();
}
waitingForMoreRecords = false;
}
protected void updateFontSize() {
Resources r = mContext.getResources();
float scaling = mLIMEPref.getFontSize();
//mHeight = (int) (r.getDimensionPixelSize(R.dimen.candidate_stripe_height) * scaling);
//mExpandButtonWidth =(int) ( r.getDimensionPixelSize(R.dimen.candidate_expand_button_width) * scaling);
mVerticalPadding =(int)( r.getDimensionPixelSize(R.dimen.candidate_vertical_padding)* scaling);
mPaint.setTextSize(r.getDimensionPixelSize(R.dimen.candidate_font_size)* scaling);
nPaint.setTextSize(r.getDimensionPixelSize(R.dimen.candidate_number_font_size)* scaling);
if(DEBUG) Log.i(TAG, "updateFontSize(), scaling=" + scaling +", mVerticalPadding=" + mVerticalPadding);
}
private void doHideCandidatePopup() {
if(DEBUG)
Log.i(TAG,"doHideCandidatePopup()");
if(mCandidatePopupWindow!=null && mCandidatePopupWindow.isShowing()) {
mCandidatePopupWindow.dismiss();
resetWidth();
}
candidateExpanded = false;
invalidate();
requestLayout();
}
private void resetWidth() {
if(DEBUG)
Log.i(TAG,"resetWidth() mHieght:" + mHeight);
int candiWidth = mScreenWidth;
if(mTotalWidth > mScreenWidth) candiWidth -= mExpandButtonWidth;
this.setLayoutParams(new LinearLayout.LayoutParams(
candiWidth, mHeight));
}
public void doUpdateCandidatePopup(){
if(DEBUG)
Log.i(TAG, "doUpdateCandidatePopup(), mHeight:" + mHeight);
//Jeremy '11,8.27 do vibrate and sound on candidateview expand button pressed.
if(!candidateExpanded)
mService.doVibrateSound(0);
candidateExpanded =true;
requestLayout();
checkHasMoreRecords();
if(mCandidatePopupWindow == null){
mCandidatePopupWindow = new PopupWindow(mContext);
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
mCandidatePopupContainer = inflater.inflate(R.layout.candidatepopup, null);
mCandidatePopupWindow.setContentView(mCandidatePopupContainer);
View closeButton = mCandidatePopupContainer.findViewById(R.id.closeButton);
if (closeButton != null) closeButton.setOnClickListener(this);
ImageButton btnClose = (ImageButton) mCandidatePopupContainer.findViewById(R.id.closeButton);
btnClose.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
mCloseButtonHeight =btnClose.getMeasuredHeight();
mPopupScrollView=(ScrollView)mCandidatePopupContainer.findViewById(R.id.sv);
CandidateExpandedView popupCandidate =
(CandidateExpandedView)mCandidatePopupContainer.findViewById(R.id.candidatePopup);
popupCandidate.setParentCandidateView(this);
popupCandidate.setParentScrollView(mPopupScrollView);
popupCandidate.setService(mService);
mPopupCandidateView = popupCandidate;
}
if(mSuggestions.size()==0) return;
mCandidatePopupWindow.setContentView(mCandidatePopupContainer);
int [] offsetOnScreen = new int[2];
this.getLocationOnScreen(offsetOnScreen);
mPopupCandidateView.setSuggestions(mSuggestions);
mPopupCandidateView.prepareLayout();
mPopupCandidateView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
/*int popHeight = 3 * mHeight + mCloseButtonHeight;
boolean upperExpand = true;
if(offsetOnScreen[1] < popHeight){
popHeight = mScreenHeight - offsetOnScreen[1] ;
if(mPopupCandidateView.getMeasuredHeight()+mCloseButtonHeight < popHeight)
popHeight = mPopupCandidateView.getMeasuredHeight()+ mCloseButtonHeight;
upperExpand = false;
}else{
this.setLayoutParams(
new LinearLayout.LayoutParams(mScreenWidth - mExpandButtonWidth, popHeight));
}*/
int popHeight = mScreenHeight - offsetOnScreen[1] ;
if(mPopupCandidateView.getMeasuredHeight()+mCloseButtonHeight < popHeight)
popHeight = mPopupCandidateView.getMeasuredHeight()+ mCloseButtonHeight;
if(!hasRoomForExpanding() ){
popHeight = 3 * mHeight + mCloseButtonHeight;
if(DEBUG)
Log.i(TAG, "doUpdateCandidatePopup(), " +
"no enough room for expanded view, expand self first. newHeight:" + popHeight);
if(mPopupCandidateView.getMeasuredHeight()+mCloseButtonHeight < popHeight)
popHeight = mPopupCandidateView.getMeasuredHeight()+ mCloseButtonHeight;
this.setLayoutParams(
new LinearLayout.LayoutParams(mScreenWidth - mExpandButtonWidth, popHeight));
}
if(DEBUG)
Log.i(TAG, "doUpdateCandidatePopup(), mHeight=" + mHeight
+ ", getHeight() = " + getHeight()
+ ", mPopupCandidateView.getHeight() = " + mPopupCandidateView.getHeight()
+ ", mPopupScrollView.getHeight() = " + mPopupScrollView.getHeight()
+ ", offsetOnScreen[1] = " + offsetOnScreen[1]
+ ", popHeight = " + popHeight
+ ", CandidateExpandedView.measureHeight = " + mPopupCandidateView.getMeasuredHeight()
+ ", btnClose.getMeasuredHeight() = " + mCloseButtonHeight
);
if(mCandidatePopupWindow.isShowing()){
if(DEBUG)
Log.i(TAG,"doUpdateCandidatePopup(),mCandidatePopup.isShowing ");
mCandidatePopupWindow.update(mScreenWidth, popHeight);
}
else{
mCandidatePopupWindow.setWidth(mScreenWidth);
mCandidatePopupWindow.setHeight(popHeight);
mCandidatePopupWindow.showAsDropDown(this, 0, -getHeight());
mPopupScrollView.scrollTo(0, 0);
}
//Jeremy '12,5,31 do update layoutparams after popupWindow update or creation.
mPopupCandidateView.setLayoutParams(
new ScrollView.LayoutParams( ScrollView.LayoutParams.MATCH_PARENT
, popHeight- mCloseButtonHeight));
mPopupScrollView.setLayoutParams(
new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT
, popHeight- mCloseButtonHeight));
}
public void setComposingText(String composingText){
if(DEBUG)
Log.i(TAG,"setComposingText():composingText:"+composingText);
if(!composingText.trim().equals("")){
//mComposingText=composingText;
mHandler.setComposing(composingText, 0);
showComposing(composingText);
}else{
//mComposingText = "";
//mHandler.dismissComposing(0);
hideComposing();
}
}
/**
* Jeremy '12,6,2 separated from doupdateComposing
*/
public void doSetComposing(String composingText) {
if(DEBUG)
Log.i(TAG,"doSetComposing():"+composingText + "this.isShown()" + this.isShown());
// Composing buffer textView
if(mComposingTextPopup==null){
LayoutInflater inflater
= (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mComposingTextPopup = new PopupWindow(mContext);
mComposingTextView = (TextView) inflater.inflate(R.layout.composingtext, null);
mComposingTextPopup.setWindowLayoutMode(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
mComposingTextPopup.setContentView(mComposingTextView);
mComposingTextPopup.setBackgroundDrawable(null);
}
if (composingText!=null ) {
mComposingTextPopup.setContentView(mComposingTextView);
mComposingTextView.setText(composingText);
mComposingTextView.setTextSize(
mContext.getResources().getDimensionPixelSize(R.dimen.composing_text_size)
*mLIMEPref.getFontSize());
}else
return;
mComposingTextView.invalidate(); //Jeremy '12,6,2 invalidate and measure so as to get correct height and width later.
mComposingTextView.setVisibility(VISIBLE);
mComposingTextView.measure(
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
final int popupWidth = mComposingTextView.getMeasuredWidth();
final int popupHeight = mComposingTextView.getMeasuredHeight();
int [] offsetInWindow = new int[2];
this.getLocationInWindow(offsetInWindow);
int mPopupComposingY = offsetInWindow[1];
int mPopupComposingX = 0;
mPopupComposingY -= popupHeight;
if (!mComposingTextPopup.isShowing()) {
mComposingTextPopup.setWidth(popupWidth);
mComposingTextPopup.setHeight(popupHeight);
mComposingTextPopup.showAtLocation(this, Gravity.NO_GRAVITY, mPopupComposingX,mPopupComposingY );
}
}
/**
* Update composing to correct location with a delay after setComposing.
*/
public void doUpdateComposing(){
if(DEBUG)
Log.i(TAG,"doUpdateComposing(): this.isShown()" + this.isShown());
final int popupWidth = mComposingTextView.getMeasuredWidth(); //Jeremy '12,6,2 use getWidth and getHeight instead
final int popupHeight = mComposingTextView.getMeasuredHeight();
int [] offsetInWindow = new int[2];
this.getLocationInWindow(offsetInWindow);
int mPopupComposingY = offsetInWindow[1];
int mPopupComposingX = 0;
// Show popup windows at the location of cursor Jeremy '11,7,25
// Rely on onCursorUpdate() of inputmethod service, which is not implemented on standard android
// Working on htc implementations
// Not working on htc ics 4.0. removed by jeremy '12,4,3
/*
if( offsetInWindow[1] == 0 && cursorRect != null){
int [] offsetOnScreen = new int[2];
this.getLocationOnScreen(offsetOnScreen);
if(DEBUG)
Log.i(TAG, "doUpdateComposing(): candidateview offsetInWindow x:"
+offsetInWindow[0]
+ ", offsetinwindow y:" +offsetInWindow[1]
+ ", cursor.top=" + cursorRect.top
+ ", cursor.left=" + cursorRect.left);
mPopupComposingX = cursorRect.right;
mPopupComposingY -= offsetOnScreen[1]- cursorRect.top - popupHeight;
if(mPopupComposingY > -popupHeight){
mPopupComposingY -= 2* popupHeight;
}
if(mPopupComposingY > 0){
mPopupComposingY= -popupHeight;
}
if(DEBUG)
Log.i(TAG, "doUpdateComposing(): candidateview offsetOnScreen x:"
+offsetOnScreen[0] + ". y:" +offsetOnScreen[1]);
}else{*/
mPopupComposingY -= popupHeight;
//}
if(DEBUG)
Log.i(TAG, "doUpdateComposing():mPopupComposingX:" +mPopupComposingX
+ ". mPopupComposingY:" +mPopupComposingY
+". popupWidth = " + popupWidth
+". popupHeight = " + popupHeight
+ ". mComposingTextPopup.isShowing()=" + mComposingTextPopup.isShowing() );
if (mComposingTextPopup.isShowing()) {
mComposingTextPopup.update(mPopupComposingX, mPopupComposingY,
popupWidth, popupHeight);
} else {
mComposingTextPopup.setWidth(popupWidth);
mComposingTextPopup.setHeight(popupHeight);
mComposingTextPopup.showAtLocation(this, Gravity.NO_GRAVITY, mPopupComposingX,
mPopupComposingY );
}
}
public void showComposing(String composingText) {
if(DEBUG)
Log.i(TAG, "showComposing()");
//jeremy '12,6,3 moved the creation of mComposingTextPopup and mComposingTextView from doUpdateComposing
//Jeremy '12,4,8 to avoid fc when hard keyboard is engaged and candidateview is not shown
if(!this.isShown()) return;
mHandler.updateComposing(200); //Jeremy '12,6,3 dealy for 200ms after setcomposing
}
public void hideComposing() {
if(DEBUG)
Log.i(TAG, "hidecomposing()");
mHandler.dismissComposing(200); //Jeremy '12,6,3 the same delay as showComposing to avoid showed after hided
}
public void showCandidatePopup(){
if(DEBUG)
Log.i(TAG, "showCandidatePopup()");
mHandler.showCandidatePopup(0);
}
public void hideCandidatePopup() {
if(DEBUG)
Log.i(TAG, "hideCandidatePopup()");
mHandler.dismissCandidatePopup(0);
}
public boolean isCandidateExpanded(){
return candidateExpanded;
}
private boolean mHasroomForExpanding = true;
public boolean hasRoomForExpanding(){
if(!mCandidatePopupWindow.isShowing()){
int [] offsetOnScreen = new int[2];
this.getLocationOnScreen(offsetOnScreen);
mHasroomForExpanding = (mScreenHeight - offsetOnScreen[1]) > 2 * mHeight;
}
return mHasroomForExpanding;
}
/**
* A connection back to the service to communicate with the text field
* @param listener
*/
public void setService(LIMEService listener) {
mService = listener;
}
@Override
public int computeHorizontalScrollRange() {
return mTotalWidth;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if(DEBUG)
Log.i(TAG,"onMeasure()");
int measuredWidth = resolveSize(mTotalWidth, widthMeasureSpec);
// Get the desired height of the icon menu view (last row of items does
// not have a divider below)
//if(padding == null) padding = new Rect();
//mSelectionHighlight.getPadding(padding);
//final int desiredHeight = ((int)mPaint.getTextSize()) + mVerticalPadding
//+ padding.top + padding.bottom;
final int desiredHeight = mHeight;//((int)mPaint.getTextSize());
// Maximum possible width and desired height
setMeasuredDimension(measuredWidth,
resolveSize(desiredHeight, heightMeasureSpec));
}
/**
* If the canvas is null, then only touch calculations are performed to pick the target
* candidate.
*/
@Override
protected synchronized void onDraw(Canvas canvas) {
if (mSuggestions == null) return;
if(DEBUG)
Log.i(TAG, "Candidateview:OnDraw():Suggestion mCount:" + mCount+" mSuggestions.size:" + mSuggestions.size());
mTotalWidth = 0;
updateFontSize();
if (mBgPadding == null) {
mBgPadding = new Rect(0, 0, 0, 0);
if (getBackground() != null) {
getBackground().getPadding(mBgPadding);
}
}
//final int mCount = mSuggestions.size();
final int height = mHeight;//= getHeight();
final Rect bgPadding = mBgPadding;
final Paint paint = mPaint;
final Paint npaint = nPaint;
final int touchX = mTouchX;
final int scrollX = getScrollX();
final boolean scrolled = mScrolled;
//final boolean typedWordValid = mTypedWordValid;
final int y = (int) (((height - mPaint.getTextSize()) / 2) - mPaint.ascent());
// Modified by jeremy '10, 3, 29. Update mselectedindex if touched and build wordX[i] and wordwidth[i]
int x = 0;
final int count = mCount; //Cache count here '11,8,18
for (int i = 0; i < count; i++) {
//if(DEBUG)
// Log.i(TAG, "Candidateview:OnDraw():updating:" + i );
if(count!=mCount || mSuggestions.size()==0) return; // mSuggestion is updated, force abort
String suggestion = mSuggestions.get(i).getWord();
float textWidth = paint.measureText(suggestion);
final int wordWidth = (int) textWidth + X_GAP * 2;
mWordX[i] = x;
mWordWidth[i] = wordWidth;
if (touchX + scrollX >= x && touchX + scrollX < x + wordWidth && !scrolled) {
mSelectedIndex = i;}
x += wordWidth;
}
mTotalWidth = x;
requestLayout();
//Jeremy '11,8,11. If the candidate list is within 1 page and has more records, get full records first.
if(mTotalWidth < this.getWidth() ) checkHasMoreRecords();
// Moved from above by jeremy '10 3, 29. Paint mselectedindex in highlight here
if (canvas != null && mSelectedIndex >=0) {
canvas.translate(mWordX[mSelectedIndex], 0);
mSelectionHighlight.setBounds(0, bgPadding.top, mWordWidth[mSelectedIndex], height);
mSelectionHighlight.draw(canvas);
canvas.translate(-mWordX[mSelectedIndex], 0);
}
// Paint all the suggestions and lines.
if (canvas != null) {
for (int i = 0; i < count; i++) {
if(count!=mCount || mSuggestions.size()==0) break;
//if(DEBUG)
// Log.i(TAG, "Candidateview:OnDraw():i:" + i + " Drawing:" + mSuggestions.get(i).getWord() );
//if ((i == 1 && !typedWordValid) || (i == 0 && typedWordValid)) {
// paint.setFakeBoldText(true);
// paint.setColor(mColorRecommended);
String suggestion = mSuggestions.get(i).getWord();
int c = i+1;
if(mSuggestions.get(i).isDictionary()){
npaint.setColor(mColorRecommended);
paint.setColor(mColorDictionary);
}else{
npaint.setColor(mColorOther);
if(i == 0){
if(mSelectedIndex == 0) paint.setColor(mColorInverted);
else paint.setColor(mColorRecommended);
} else if (i != 0) {
paint.setColor(mColorOther);
}
}
canvas.drawText(suggestion, mWordX[i] + X_GAP, y, paint);
if(mShowNumber){
//Jeremy '11,6,17 changed from <=10 to mDisplaySekley length. The length maybe 11 or 12 if shifted with space.
if(c <= mDisplaySelkey.length()){
//Jeremy '11,6,11 Drawing text using relative font dimensions.
canvas.drawText(mDisplaySelkey.substring(c-1, c),
mWordX[i] + mWordWidth[i] - height * 0.3f , height * 0.4f, npaint);
}
}
paint.setColor(mColorOther);
canvas.drawLine(mWordX[i] + mWordWidth[i] + 0.5f, bgPadding.top,
mWordX[i] + mWordWidth[i] + 0.5f, height + 1, paint);
paint.setFakeBoldText(false);
}
if (mTargetScrollX != getScrollX()) {
scrollToTarget();
}
//showComposing(mComposingText);
}
}
private boolean checkHasMoreRecords(){
if(DEBUG)
Log.i(TAG, "checkHasMoreRecords(), waitingForMoreRecords = " + waitingForMoreRecords );
if(waitingForMoreRecords) return false; //Jeremy '12,7,6 avoid repeated calls of requestFullrecords().
if(mSuggestions!=null && mSuggestions.size()>0 &&
mSuggestions.get(mSuggestions.size()-1).getCode() !=null &&
mSuggestions.get(mSuggestions.size()-1).getCode().equals("has_more_records")){
waitingForMoreRecords=true;
Thread UpadtingThread = new Thread(){
public void run() {
mService.requestFullRecords(mSuggestions.get(0).isDictionary());
}
};
UpadtingThread.start();
return true;
}
return false;
}
private void scrollToTarget() {
int sx = getScrollX();
if (mTargetScrollX > sx) {
sx += SCROLL_PIXELS;
if (sx >= mTargetScrollX) {
sx = mTargetScrollX;
requestLayout();
}
} else {
sx -= SCROLL_PIXELS;
if (sx <= mTargetScrollX) {
sx = mTargetScrollX;
requestLayout();
}
}
scrollTo(sx, getScrollY());
invalidate();
}
public void setSuggestions(List<Mapping> suggestions){
setSuggestions(suggestions, false, false, "");
}
public void setSuggestions(List<Mapping> suggestions, boolean showNumber, boolean typedWordValid, String displaySelkey) {
mDisplaySelkey = displaySelkey;
setSuggestions(suggestions, showNumber, typedWordValid);
}
public synchronized void setSuggestions(List<Mapping> suggestions, boolean showNumber, boolean typedWordValid) {
//clear();
//Jeremy '11,8,14 moved from clear();
if(DEBUG)
Log.i(TAG,"setSuggestions()");
Resources res = mContext.getResources();
mHeight = (int) (res.getDimensionPixelSize(R.dimen.candidate_stripe_height) *mLIMEPref.getFontSize()); //move from constructor by Jeremy '12,5,6
currentX = 0;
mTouchX = OUT_OF_BOUNDS;
mCount =0;
mSelectedIndex =-1;
if(mLIMEPref.getDisablePhysicalSelKeyOption()){
showNumber = true;
}
//TODO: isAndroid3 should be replace as something which can detect working on tablets but not phones.
mShowNumber = showNumber && isAndroid3;
if(mShowNumber)
X_GAP = (int) (res.getDimensionPixelSize(R.dimen.candidate_font_size)*0.35f);//13;
else
X_GAP = (int) (res.getDimensionPixelSize(R.dimen.candidate_font_size)*0.25f);;
if (suggestions != null) {
mSuggestions = new LinkedList<Mapping>(suggestions);
if(mSuggestions != null && mSuggestions.size() > 0){
//setBackgroundColor(bgcolor);
// Add by Jeremy '10, 3, 29
mCount = mSuggestions.size();
if(mCount > MAX_SUGGESTIONS) mCount = MAX_SUGGESTIONS;
if(DEBUG)
Log.i(TAG, "setSuggestions():mSuggestions.size():" + mSuggestions.size()
+ " mCount=" + mCount);
if(mSuggestions.get(0).isDictionary()){
// no default selection for related words
mSelectedIndex = -1;
/* }else if(mCount > 1 && mSuggestions.get(1).getId() !=null && // the suggestion is not from relatedlist.
mSuggestions.get(0).getWord().toLowerCase()
.equals(mSuggestions.get(1).getCode().trim())) { // exact match
// default selection on suggestions 1 (0 is typed English in mixed English mode)
mSelectedIndex = 1;
}else if(mCount > 1 && mSuggestions.get(1).getId() !=null &&
mService.activeIM.equals("phonetic")&&
(mSuggestions.get(0).getWord().trim().toLowerCase()
.equals(mSuggestions.get(1).getCode().trim())||
mSuggestions.get(0).getWord().trim().toLowerCase()
.equals(mSuggestions.get(1).getCode().trim().replaceAll("[3467]", ""))) ){
*/
//Jeremy '12,5,31 If mSuggestions.get(0).getRelated() is true means no exact match result found,
//set default candidate as mixed English code,
//otherwise set default suggestion to the first one of the result list.
}else if(mCount > 1 && !mSuggestions.get(0).getRelated()){
mSelectedIndex = 1;
}else {
mSelectedIndex = 0;
}
}else{
if(DEBUG)
Log.i(TAG, "setSuggestions():mSuggestions=null");
//setBackgroundColor(0);
}
//mTypedWordValid = typedWordValid;
//scrollTo(0, 0);
//mTargetScrollX = 0;
// Compute the total width
}else{
mSuggestions = new LinkedList<Mapping>();
hideCandidatePopup();
}
mHandler.updateUI(0);
/*//Jeremy '11,8,18 moved to mHandler to be thread-safe.
if(mCandidatePopup != null && mCandidatePopup.isShowing()){
showCandidatePopup();
}else{
onDraw(null);
invalidate();
}
requestLayout();*/
}
public void clear() {
if(DEBUG) Log.i(TAG, "clear()");
//mHeight =0; //Jeremy '12,5,6 hide candidate bar when candidateview is fixed.
mSuggestions = EMPTY_LIST;
// Jeremy 11,8,14 close all popup on clear
setComposingText("");
mTargetScrollX = 0;
hideComposing();
hideCandidatePopup();
mHandler.updateUI(0);
mHeight = (int) (mContext.getResources().getDimensionPixelSize(
R.dimen.candidate_stripe_height) *mLIMEPref.getFontSize()); //restore the height Jeremy '12,5,24
}
//Jeremy '12,5,6 hide candidate bar when candidateview is fixed.
public void forceHide() {
clear();
mHeight =0;
}
@Override
public boolean onTouchEvent(MotionEvent me) {
if(DEBUG)
Log.i(TAG,"OnTouchEvent() action = " + me.getAction());
if (mGestureDetector!=null && mGestureDetector.onTouchEvent(me)) {
if(DEBUG)
Log.i(TAG,"OnTouchEvent() event processed by mGestureDetector");
return true;
}
int action = me.getAction();
int x = (int) me.getX();
int y = (int) me.getY();
mTouchX = x;
switch (action) {
case MotionEvent.ACTION_DOWN:
mScrolled = false;
invalidate();
break;
case MotionEvent.ACTION_MOVE:
if (y <= 0) {
// Fling up!?
if (mSelectedIndex >= 0) {
takeSelectedSuggestion(true);
mSelectedIndex = -1;
}
}
invalidate();
break;
case MotionEvent.ACTION_UP:
if(DEBUG)
Log.i(TAG,"OnTouchEvent():MotionEvent.ACTION_UP, mScrolled="+mScrolled +"; mSelectedIndex = " + mSelectedIndex);
if (!mScrolled) {
if (mSelectedIndex >= 0) {
takeSelectedSuggestion(true);
}
}
mSelectedIndex = -1;
removeHighlight();
requestLayout();
if(!hasSlide){
if(goLeft){
scrollPrev();
}
if(goRight){
scrollNext();
}
}
break;
}
return true;
}
public void scrollPrev() {
int i = 0;
//final int mCount = mSuggestions.size();
int firstItem = 0; // Actually just before the first item, if at the boundary
while (i < mCount) {
if (mWordX[i] < currentX
&& mWordX[i] + mWordWidth[i] >= currentX - 1) {
firstItem = i;
break;
}
i++;
}
int leftEdge = mWordX[firstItem] + mWordWidth[firstItem] - getWidth();
if (leftEdge < 0) {
leftEdge = 0;
currentX = leftEdge;
}else{
currentX = leftEdge;
}
updateScrollPosition(leftEdge);
}
public void scrollNext() {
if(DEBUG)
Log.i(TAG, "scrollNext(), currentX = " + currentX + ", mSelectedIndex = " + mSelectedIndex);
checkHasMoreRecords(); //Jeremy '12,7,6 check if has more records before scroll
int i = 0;
int targetX = currentX;
//final int mCount = mSuggestions.size();
int rightEdge = currentX + getWidth();
while (i < mCount) {
if (mWordX[i] <= rightEdge &&
mWordX[i] + mWordWidth[i] >= rightEdge) {
targetX = Math.min(mWordX[i], mTotalWidth - getWidth());
currentX = mWordX[i];
break;
}
i++;
}
if(DEBUG)
Log.i(TAG, "scrollNext(), new currentX = " + currentX + ", new mSelectedIndex = " + mSelectedIndex);
updateScrollPosition(targetX);
}
private void updateScrollPosition(int targetX) {
if (targetX != mTouchX) {
mTargetScrollX = targetX;
requestLayout();
invalidate();
mScrolled = true;
}
}
//Add by Jeremy '10, 3, 29 for DPAD (physical keyboard) selection.
public void selectNext() {
if(DEBUG)
Log.i(TAG, "selectNext(), currentX = " + currentX + ", mSelectedIndex = " + mSelectedIndex);
if (mSuggestions == null) return;
if(mCandidatePopupWindow!=null && mCandidatePopupWindow.isShowing()){
mPopupCandidateView.selectNext();
}else{
if(mSelectedIndex < mCount-1){
mSelectedIndex++;
if(mWordX[mSelectedIndex] + mWordWidth[mSelectedIndex] > currentX + getWidth())
scrollNext();
//Jeremy '12,7,6 if the selected index is not in current visible area, set the selected index to the fist item visible
int rightEdge = currentX + getWidth();
if(mWordX[mSelectedIndex] < currentX ||
mWordX[mSelectedIndex] + mWordWidth[mSelectedIndex] > rightEdge){
for(int i = 0; i < mCount-1 ; i++)
if(mWordX[i] >= currentX){
mSelectedIndex = i;
break;
}
}
}
invalidate();
}
}
public void selectPrev() {
if(DEBUG)
Log.i(TAG, "selectPrev(), currentX = " + currentX + ", mSelectedIndex = " + mSelectedIndex);
if (mSuggestions == null) return;
if(mCandidatePopupWindow!=null && mCandidatePopupWindow.isShowing()){
mPopupCandidateView.selectPrev();
}else{
if(mSelectedIndex > 0) {
mSelectedIndex--;
if(mWordX[mSelectedIndex] < currentX) scrollPrev();
}
//Jeremy '12,7,6 if the selected index is not in current visible area, set the selected index to the last item visible
int rightEdge = currentX + getWidth();
if(mSelectedIndex == -1 ||
mWordX[mSelectedIndex] < currentX ||
mWordX[mSelectedIndex] + mWordWidth[mSelectedIndex] > rightEdge){
for(int i = mCount-2; i < mCount-1 ; i--)
if(mWordX[i] + mWordWidth[i] <= rightEdge){
mSelectedIndex = i;
break;
}
}
invalidate();
}
}
//Jeremy '11,8,28
public void selectNextRow(){
if (mSuggestions == null) return;
if(mCandidatePopupWindow!=null && mCandidatePopupWindow.isShowing())
mPopupCandidateView.selectNextRow();
else if(mScreenWidth < mTotalWidth)
showCandidatePopup();
}
public void selectPrevRow() {
if (mSuggestions == null) return;
if(mCandidatePopupWindow!=null && mCandidatePopupWindow.isShowing())
mPopupCandidateView.selectPrevRow();
}
public boolean takeSuggstionAtIndex(int index){
if(DEBUG){
Log.i(TAG, "takeSuggestion():mSelectedIndex:" + mSelectedIndex);
}
if (mSuggestions != null && index >= 0 && index <= mSuggestions.size() ) {
mService.pickCandidateManually(index);
return true; // Selection picked
}else
return false;
}
public boolean takeSelectedSuggestion(){
return this.takeSelectedSuggestion(false);
}
public boolean takeSelectedSuggestion(boolean vibrateSound){
if(DEBUG){
Log.i(TAG, "takeSelectedSuggestion():mSelectedIndex:" + mSelectedIndex);
}
//Jeremy '11,9,1 do vibrate and sound on suggestion picked from candidateview
if(vibrateSound) mService.doVibrateSound(0);
hideComposing(); //Jeremy '12,5,6
if(mCandidatePopupWindow!=null && mCandidatePopupWindow.isShowing()){
hideCandidatePopup();
return takeSuggstionAtIndex(mPopupCandidateView.mSelectedIndex);
}else
return takeSuggstionAtIndex(mSelectedIndex);
}
/**
* For flick through from keyboard, call this method with the x coordinate of the flick
* gesture.
* @param x
*/
/*public void takeSuggestionAt(float x) {
mTouchX = (int) x;
// To detect candidate
onDraw(null);
takeSelectedSuggestion();
invalidate();
}*/
private void removeHighlight() {
mTouchX = OUT_OF_BOUNDS;
invalidate();
}
@Override
public void onDetachedFromWindow() {
if(DEBUG) Log.i(TAG,"onDetachedFromWindow() ");
super.onDetachedFromWindow();
hideComposing();
hideCandidatePopup();
}
public void onUpdateCursor(Rect newCursor) {
cursorRect = newCursor;
invalidate();
}
@Override
public void onClick(View v) {
//Jeremy '11,8.27 do vibrate and sound on candidateexpandedview close button pressed.
mService.doVibrateSound(0);
hideCandidatePopup();
}
}