1/*
2 * Copyright (C) 2008-2012  OMRON SOFTWARE Co., Ltd.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package jp.co.omronsoft.openwnn;
17
18import android.app.AlertDialog;
19import android.content.Context;
20import android.content.DialogInterface;
21import android.content.SharedPreferences;
22import android.content.res.Resources;
23import android.graphics.drawable.Drawable;
24import android.media.AudioManager;
25import android.os.Handler;
26import android.os.Message;
27import android.os.Vibrator;
28import android.text.Layout;
29import android.text.SpannableString;
30import android.text.Spanned;
31import android.text.style.AbsoluteSizeSpan;
32import android.text.style.AlignmentSpan;
33import android.util.DisplayMetrics;
34import android.util.Log;
35import android.view.KeyEvent;
36import android.view.LayoutInflater;
37import android.view.MotionEvent;
38import android.view.View;
39import android.view.ViewGroup;
40import android.view.View.OnClickListener;
41import android.view.View.OnLongClickListener;
42import android.widget.Button;
43import android.widget.ImageView;
44import android.widget.LinearLayout;
45import android.widget.HorizontalScrollView;
46import android.widget.TextView;
47
48import java.util.ArrayList;
49
50/**
51 * The default candidates view manager using {@link android.widget.EditText}.
52 *
53 * @author Copyright (C) 2011 OMRON SOFTWARE CO., LTD.  All Rights Reserved.
54 */
55public class TextCandidates1LineViewManager extends CandidatesViewManager {
56
57    /** displayCandidates() normal display */
58    private static final int IS_NEXTCANDIDATE_NORMAL = 1;
59    /** displayCandidates() delay display */
60    private static final int IS_NEXTCANDIDATE_DELAY = 2;
61    /** displayCandidates() display end */
62    private static final int IS_NEXTCANDIDATE_END = 3;
63
64    /** Delay of set candidate */
65    private static final int SET_CANDIDATE_DELAY = 50;
66    /** Delay Millis */
67    private static final int CANDIDATE_DELAY_MILLIS = 500;
68
69    /** Scroll distance */
70    private static final float SCROLL_DISTANCE = 0.9f;
71
72    /** Body view of the candidates list */
73    private ViewGroup  mViewBody;
74    /** Scroller */
75    private HorizontalScrollView mViewBodyScroll;
76    /** Left more button */
77    private ImageView mLeftMoreButton;
78    /** Right more button */
79    private ImageView mRightMoreButton;
80    /** Candidate view */
81    private LinearLayout mViewCandidateList;
82
83    /** {@link OpenWnn} instance using this manager */
84    private OpenWnn mWnn;
85    /** View type (VIEW_TYPE_NORMAL or VIEW_TYPE_FULL or VIEW_TYPE_CLOSE) */
86    private int mViewType;
87
88    /** view width */
89    private int mViewWidth;
90    /** Minimum width of candidate view */
91    private int mCandidateMinimumWidth;
92    /** Minimum height of candidate view */
93    private int mCandidateMinimumHeight;
94
95    /** Minimum width of candidate view */
96    private static final int CANDIDATE_MINIMUM_WIDTH = 48;
97
98    /** Whether hide the view if there is no candidate */
99    private boolean mAutoHideMode;
100    /** The converter to get candidates from and notice the selected candidate to. */
101    private WnnEngine mConverter;
102    /** Limitation of displaying candidates */
103    private int mDisplayLimit;
104
105    /** Vibrator for touch vibration */
106    private Vibrator mVibrator = null;
107    /** AudioManager for click sound */
108    private AudioManager mSound = null;
109
110    /** Number of candidates displaying */
111    private int mWordCount;
112    /** List of candidates */
113    private ArrayList<WnnWord> mWnnWordArray;
114
115    /** Character width of the candidate area */
116    private int mLineLength = 0;
117    /** Maximum width of candidate view */
118    private int mCandidateMaxWidth = 0;
119    /** general information about a display */
120    private final DisplayMetrics mMetrics = new DisplayMetrics();
121    /** Focus is none now */
122    private static final int FOCUS_NONE = -1;
123    /** Handler for set  Candidate */
124    private static final int MSG_SET_CANDIDATES = 1;
125    /** List of textView for CandiData List */
126    private ArrayList<TextView> mTextViewArray = new ArrayList<TextView>();
127    /** Now focus textView index */
128    private int mCurrentFocusIndex = FOCUS_NONE;
129    /** Focused View */
130    private View mFocusedView = null;
131    /** Focused View Background */
132    private Drawable mFocusedViewBackground = null;
133    /** Scale up text size */
134    private AbsoluteSizeSpan mSizeSpan;
135    /** Scale up text alignment */
136    private AlignmentSpan.Standard mCenterSpan;
137    /** Whether candidates long click enable */
138    private boolean mEnableCandidateLongClick = true;
139
140   /** {@code Handler} Handler for focus Candidate wait delay */
141    private Handler mHandler = new Handler() {
142            @Override public void handleMessage(Message msg) {
143
144            switch (msg.what) {
145                case MSG_SET_CANDIDATES:
146                    displayCandidatesDelay(mConverter);
147                    break;
148
149                default:
150                    break;
151                }
152            }
153        };
154
155    /** Event listener for touching a candidate */
156    private OnClickListener mCandidateOnClick = new OnClickListener() {
157        public void onClick(View v) {
158            if (!v.isShown()) {
159                return;
160            }
161            playSoundAndVibration();
162
163            if (v instanceof CandidateTextView) {
164                CandidateTextView text = (CandidateTextView)v;
165                int wordcount = text.getId();
166                WnnWord word = getWnnWord(wordcount);
167                clearFocusCandidate();
168                selectCandidate(word);
169            }
170        }
171    };
172
173    /** Event listener for long-clicking a candidate */
174    private OnLongClickListener mCandidateOnLongClick = new OnLongClickListener() {
175        public boolean onLongClick(View v) {
176            if (!v.isShown()) {
177                return true;
178            }
179
180            if (!mEnableCandidateLongClick) {
181                return false;
182            }
183
184            clearFocusCandidate();
185
186            int wordcount = ((TextView)v).getId();
187            mWord = mWnnWordArray.get(wordcount);
188
189            displayDialog(v, mWord);
190            return true;
191        }
192    };
193
194    /**
195     * Constructor
196     */
197    public TextCandidates1LineViewManager() {
198        this(300);
199    }
200
201    /**
202     * Constructor
203     *
204     * @param displayLimit      The limit of display
205     */
206    public TextCandidates1LineViewManager(int displayLimit) {
207        mDisplayLimit = displayLimit;
208        mWnnWordArray = new ArrayList<WnnWord>();
209        mAutoHideMode = true;
210        mMetrics.setToDefaults();
211    }
212
213    /**
214     * Set auto-hide mode.
215     * @param hide      {@code true} if the view will hidden when no candidate exists;
216     *                  {@code false} if the view is always shown.
217     */
218    public void setAutoHide(boolean hide) {
219        mAutoHideMode = hide;
220    }
221
222    /** @see CandidatesViewManager */
223    public View initView(OpenWnn parent, int width, int height) {
224        mWnn = parent;
225        mViewWidth = width;
226
227        Resources r = mWnn.getResources();
228
229        mCandidateMinimumWidth = (int)(CANDIDATE_MINIMUM_WIDTH * mMetrics.density);
230        mCandidateMinimumHeight = r.getDimensionPixelSize(R.dimen.candidate_layout_height);
231
232        LayoutInflater inflater = parent.getLayoutInflater();
233        mViewBody = (ViewGroup)inflater.inflate(R.layout.candidates_1line, null);
234        mViewBodyScroll = (HorizontalScrollView)mViewBody.findViewById(R.id.candview_scroll_1line);
235        mViewBodyScroll.setOverScrollMode(View.OVER_SCROLL_NEVER);
236        mViewBodyScroll.setOnTouchListener(new View.OnTouchListener() {
237            public boolean onTouch(View v, MotionEvent event) {
238                switch (event.getAction()) {
239                case MotionEvent.ACTION_DOWN:
240                case MotionEvent.ACTION_MOVE:
241                    if (mHandler.hasMessages(MSG_SET_CANDIDATES)) {
242                        mHandler.removeMessages(MSG_SET_CANDIDATES);
243                        mHandler.sendEmptyMessageDelayed(MSG_SET_CANDIDATES, CANDIDATE_DELAY_MILLIS);
244                    }
245                    break;
246
247                default:
248                    break;
249
250                }
251                return false;
252            }
253        });
254
255        mLeftMoreButton = (ImageView)mViewBody.findViewById(R.id.left_more_imageview);
256        mLeftMoreButton.setOnClickListener(new View.OnClickListener() {
257            public void onClick(View v) {
258                if (!v.isShown()) {
259                    return;
260                }
261                playSoundAndVibration();
262                if (mViewBodyScroll.getScrollX() > 0) {
263                    mViewBodyScroll.smoothScrollBy(
264                                        (int)(mViewBodyScroll.getWidth() * -SCROLL_DISTANCE), 0);
265                }
266            }
267        });
268        mLeftMoreButton.setOnLongClickListener(new View.OnLongClickListener() {
269            public boolean onLongClick(View v) {
270                if (!v.isShown()) {
271                    return false;
272                }
273                if (!mViewBodyScroll.fullScroll(View.FOCUS_LEFT)) {
274                    mViewBodyScroll.scrollTo(mViewBodyScroll.getChildAt(0).getWidth(), 0);
275                }
276                return true;
277            }
278        });
279
280        mRightMoreButton = (ImageView)mViewBody.findViewById(R.id.right_more_imageview);
281        mRightMoreButton.setOnClickListener(new View.OnClickListener() {
282            public void onClick(View v) {
283                if (!v.isShown()) {
284                    return;
285                }
286                int width = mViewBodyScroll.getWidth();
287                int scrollMax = mViewBodyScroll.getChildAt(0).getRight();
288
289                if ((mViewBodyScroll.getScrollX() + width) < scrollMax) {
290                    mViewBodyScroll.smoothScrollBy((int)(width * SCROLL_DISTANCE), 0);
291                }
292            }
293        });
294        mRightMoreButton.setOnLongClickListener(new View.OnLongClickListener() {
295            public boolean onLongClick(View v) {
296                if (!v.isShown()) {
297                    return false;
298                }
299                if (!mViewBodyScroll.fullScroll(View.FOCUS_RIGHT)) {
300                    mViewBodyScroll.scrollTo(0, 0);
301                }
302                return true;
303            }
304        });
305
306        mViewLongPressDialog = (View)inflater.inflate(R.layout.candidate_longpress_dialog, null);
307
308        /* select button */
309        Button longPressDialogButton = (Button)mViewLongPressDialog.findViewById(R.id.candidate_longpress_dialog_select);
310        longPressDialogButton.setOnClickListener(new View.OnClickListener() {
311                public void onClick(View v) {
312                    playSoundAndVibration();
313                    clearFocusCandidate();
314                    selectCandidate(mWord);
315                    closeDialog();
316                }
317            });
318
319        /* cancel button */
320        longPressDialogButton = (Button)mViewLongPressDialog.findViewById(R.id.candidate_longpress_dialog_cancel);
321        longPressDialogButton.setOnClickListener(new View.OnClickListener() {
322                public void onClick(View v) {
323                    playSoundAndVibration();
324                    mWnn.onEvent(new OpenWnnEvent(OpenWnnEvent.LIST_CANDIDATES_NORMAL));
325                    mWnn.onEvent(new OpenWnnEvent(OpenWnnEvent.UPDATE_CANDIDATE));
326                    closeDialog();
327                }
328            });
329
330        int buttonWidth = r.getDimensionPixelSize(R.dimen.candidate_layout_width);
331        mCandidateMaxWidth = (mViewWidth - buttonWidth * 2) / 2;
332
333        mSizeSpan = new AbsoluteSizeSpan(r.getDimensionPixelSize(R.dimen.candidate_delete_word_size));
334        mCenterSpan = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);
335
336        createNormalCandidateView();
337
338        setViewType(CandidatesViewManager.VIEW_TYPE_CLOSE);
339
340        return mViewBody;
341    }
342
343    /**
344     * Create normal candidate view
345     */
346    private void createNormalCandidateView() {
347        mViewCandidateList = (LinearLayout)mViewBody.findViewById(R.id.candidates_view_1line);
348
349        Context context = mViewBodyScroll.getContext();
350        for (int i = 0; i < mDisplayLimit; i++) {
351            mViewCandidateList.addView(new CandidateTextView(context,
352                                                            mCandidateMinimumHeight,
353                                                            mCandidateMinimumWidth,
354                                                            mCandidateMaxWidth));
355        }
356    }
357
358    /** @see CandidatesViewManager#getCurrentView */
359    public View getCurrentView() {
360        return mViewBody;
361    }
362
363    /** @see CandidatesViewManager#setViewType */
364    public void setViewType(int type) {
365        mViewType = type;
366
367        if (type == CandidatesViewManager.VIEW_TYPE_NORMAL) {
368            mViewCandidateList.setMinimumHeight(mCandidateMinimumHeight);
369        } else {
370            mViewCandidateList.setMinimumHeight(-1);
371            mHandler.removeMessages(MSG_SET_CANDIDATES);
372
373            if (mViewBody.isShown()) {
374                mWnn.setCandidatesViewShown(false);
375            }
376        }
377    }
378
379    /** @see CandidatesViewManager#getViewType */
380    public int getViewType() {
381        return mViewType;
382    }
383
384    /** @see CandidatesViewManager#displayCandidates */
385    public void displayCandidates(WnnEngine converter) {
386
387        mHandler.removeMessages(MSG_SET_CANDIDATES);
388
389        closeDialog();
390        clearCandidates();
391        mConverter = converter;
392
393        int isNextCandidate = IS_NEXTCANDIDATE_NORMAL;
394        while(isNextCandidate == IS_NEXTCANDIDATE_NORMAL) {
395            isNextCandidate = displayCandidatesNormal(converter);
396        }
397
398        if (isNextCandidate == IS_NEXTCANDIDATE_DELAY) {
399            isNextCandidate = displayCandidatesDelay(converter);
400        }
401
402        mViewBodyScroll.scrollTo(0,0);
403    }
404
405
406    /**
407     * Display the candidates.
408     * @param converter  {@link WnnEngine} which holds candidates.
409     */
410    private int displayCandidatesNormal(WnnEngine converter) {
411        int isNextCandidate = IS_NEXTCANDIDATE_NORMAL;
412
413        if (converter == null) {
414            return IS_NEXTCANDIDATE_END;
415        }
416
417        /* Get candidates */
418        WnnWord result = converter.getNextCandidate();
419        if (result == null) {
420            return IS_NEXTCANDIDATE_END;
421        }
422
423        mLineLength += setCandidate(result);
424        if (mLineLength >= mViewWidth) {
425            isNextCandidate = IS_NEXTCANDIDATE_DELAY;
426        }
427
428        if (mWordCount < 1) { /* no candidates */
429            if (mAutoHideMode) {
430                mWnn.setCandidatesViewShown(false);
431                return IS_NEXTCANDIDATE_END;
432            }
433        }
434
435        if (mWordCount > mDisplayLimit) {
436            return IS_NEXTCANDIDATE_END;
437        }
438
439        if (!(mViewBody.isShown())) {
440            mWnn.setCandidatesViewShown(true);
441        }
442        return isNextCandidate;
443    }
444
445    /**
446     * Display the candidates.
447     * @param converter  {@link WnnEngine} which holds candidates.
448     */
449    private int displayCandidatesDelay(WnnEngine converter) {
450        int isNextCandidate = IS_NEXTCANDIDATE_DELAY;
451
452        if (converter == null) {
453            return IS_NEXTCANDIDATE_END;
454        }
455
456        /* Get candidates */
457        WnnWord result = converter.getNextCandidate();
458        if (result == null) {
459            return IS_NEXTCANDIDATE_END;
460        }
461
462        setCandidate(result);
463
464        if (mWordCount > mDisplayLimit) {
465            return IS_NEXTCANDIDATE_END;
466        }
467
468        mHandler.sendEmptyMessageDelayed(MSG_SET_CANDIDATES, SET_CANDIDATE_DELAY);
469
470        return isNextCandidate;
471    }
472
473    /**
474     * Set the candidate for candidate view
475     * @param word set word
476     * @return int Set width
477     */
478    private int setCandidate(WnnWord word) {
479        CandidateTextView candidateTextView =
480                (CandidateTextView) mViewCandidateList.getChildAt(mWordCount);
481        candidateTextView.setCandidateTextView(word, mWordCount, mCandidateOnClick,
482                                                    mCandidateOnLongClick);
483        mWnnWordArray.add(mWordCount, word);
484        mWordCount++;
485        mTextViewArray.add(candidateTextView);
486
487        return candidateTextView.getWidth();
488    }
489
490    /**
491     * Clear the candidate view
492     */
493    private void clearNormalViewCandidate() {
494        int candidateNum = mViewCandidateList.getChildCount();
495        for (int i = 0; i < candidateNum; i++) {
496            View v = mViewCandidateList.getChildAt(i);
497            v.setVisibility(View.GONE);
498        }
499    }
500
501    /** @see CandidatesViewManager#clearCandidates */
502    public void clearCandidates() {
503        clearFocusCandidate();
504        clearNormalViewCandidate();
505
506        mLineLength = 0;
507
508        mWordCount = 0;
509        mWnnWordArray.clear();
510        mTextViewArray.clear();
511
512        if (mAutoHideMode && mViewBody.isShown()) {
513            mWnn.setCandidatesViewShown(false);
514        }
515    }
516
517    /** @see CandidatesViewManager#setPreferences */
518    public void setPreferences(SharedPreferences pref) {
519        try {
520            if (pref.getBoolean("key_vibration", false)) {
521                mVibrator = (Vibrator)mWnn.getSystemService(Context.VIBRATOR_SERVICE);
522            } else {
523                mVibrator = null;
524            }
525            if (pref.getBoolean("key_sound", false)) {
526                mSound = (AudioManager)mWnn.getSystemService(Context.AUDIO_SERVICE);
527            } else {
528                mSound = null;
529            }
530        } catch (Exception ex) {
531            Log.d("OpenWnn", "NO VIBRATOR");
532        }
533    }
534
535    /**
536     * Select a candidate.
537     * <br>
538     * This method notices the selected word to {@link OpenWnn}.
539     *
540     * @param word  The selected word
541     */
542    private void selectCandidate(WnnWord word) {
543        mWnn.onEvent(new OpenWnnEvent(OpenWnnEvent.SELECT_CANDIDATE, word));
544    }
545
546    private void playSoundAndVibration() {
547        if (mVibrator != null) {
548            try {
549                mVibrator.vibrate(5);
550            } catch (Exception ex) {
551                Log.e("OpenWnn", "TextCandidates1LineViewManager::selectCandidate Vibrator " + ex.toString());
552            }
553        }
554        if (mSound != null) {
555            try {
556                mSound.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD, 1.0f);
557            } catch (Exception ex) {
558                Log.e("OpenWnn", "TextCandidates1LineViewManager::selectCandidate Sound " + ex.toString());
559            }
560        }
561    }
562
563    /**
564     * KeyEvent action for the candidate view.
565     *
566     * @param key    Key event
567     */
568    public void processMoveKeyEvent(int key) {
569        if (!mViewBody.isShown()) {
570            return;
571        }
572
573        switch (key) {
574        case KeyEvent.KEYCODE_DPAD_LEFT:
575            moveFocus(-1);
576            break;
577
578        case KeyEvent.KEYCODE_DPAD_RIGHT:
579            moveFocus(1);
580            break;
581
582        case KeyEvent.KEYCODE_DPAD_UP:
583            moveFocus(-1);
584            break;
585
586        case KeyEvent.KEYCODE_DPAD_DOWN:
587            moveFocus(1);
588            break;
589
590        default:
591            break;
592
593        }
594    }
595
596    /**
597     * Get a flag candidate is focused now.
598     *
599     * @return the Candidate is focused of a flag.
600     */
601    public boolean isFocusCandidate(){
602        if (mCurrentFocusIndex != FOCUS_NONE) {
603            return true;
604        }
605        return false;
606    }
607
608    /**
609     * Give focus to View of candidate.
610     */
611    private void setViewStatusOfFocusedCandidate() {
612        View view = mFocusedView;
613        if (view != null) {
614            view.setBackgroundDrawable(mFocusedViewBackground);
615        }
616
617        TextView v = getFocusedView();
618        mFocusedView = v;
619        if (v != null) {
620            mFocusedViewBackground = v.getBackground();
621            v.setBackgroundResource(R.drawable.cand_back_focuse);
622
623            int viewBodyLeft = getViewLeftOnScreen(mViewBodyScroll);
624            int viewBodyRight = viewBodyLeft + mViewBodyScroll.getWidth();
625            int focusedViewLeft = getViewLeftOnScreen(v);
626            int focusedViewRight = focusedViewLeft + v.getWidth();
627
628            if (focusedViewRight > viewBodyRight) {
629                mViewBodyScroll.scrollBy((focusedViewRight - viewBodyRight), 0);
630            } else if (focusedViewLeft < viewBodyLeft) {
631                mViewBodyScroll.scrollBy((focusedViewLeft - viewBodyLeft), 0);
632            }
633        }
634    }
635
636    /**
637     * Clear focus to selected candidate.
638     */
639    private void clearFocusCandidate(){
640        View view = mFocusedView;
641        if (view != null) {
642            view.setBackgroundDrawable(mFocusedViewBackground);
643            mFocusedView = null;
644        }
645
646        mCurrentFocusIndex = FOCUS_NONE;
647
648        mWnn.onEvent(new OpenWnnEvent(OpenWnnEvent.FOCUS_CANDIDATE_END));
649    }
650
651    /**
652     * Select candidate that has focus.
653     */
654    public void selectFocusCandidate(){
655        if (mCurrentFocusIndex != FOCUS_NONE) {
656            selectCandidate(getFocusedWnnWord());
657        }
658    }
659
660    /**
661     * Get View of focus candidate.
662     */
663    private TextView getFocusedView() {
664        if (mCurrentFocusIndex == FOCUS_NONE) {
665            return null;
666        }
667        return mTextViewArray.get(mCurrentFocusIndex);
668    }
669
670    /**
671     * Move the focus to next candidate.
672     *
673     * @param direction  The direction of increment or decrement.
674     */
675    private void moveFocus(int direction) {
676        boolean isStart = (mCurrentFocusIndex == FOCUS_NONE);
677        int size = mTextViewArray.size();
678        int index = isStart ? 0 : (mCurrentFocusIndex + direction);
679
680        if (index < 0) {
681            index = size - 1;
682        } else {
683            if (index >= size) {
684                index = 0;
685            }
686        }
687
688        mCurrentFocusIndex = index;
689        setViewStatusOfFocusedCandidate();
690
691        if (isStart) {
692            mWnn.onEvent(new OpenWnnEvent(OpenWnnEvent.FOCUS_CANDIDATE_START));
693        }
694    }
695
696    /**
697     * Get view top position on screen.
698     *
699     * @param view target view.
700     * @return int view top position on screen
701     */
702    private int getViewLeftOnScreen(View view) {
703        int[] location = new int[2];
704        view.getLocationOnScreen(location);
705        return location[0];
706    }
707
708    /** @see CandidatesViewManager#getFocusedWnnWord */
709    public WnnWord getFocusedWnnWord() {
710        return getWnnWord(mCurrentFocusIndex);
711    }
712
713    /**
714     * Get WnnWord.
715     *
716     * @return WnnWord word
717     */
718    public WnnWord getWnnWord(int index) {
719        return mWnnWordArray.get(index);
720    }
721
722    /** @see CandidatesViewManager#setCandidateMsgRemove */
723    public void setCandidateMsgRemove() {
724        mHandler.removeMessages(MSG_SET_CANDIDATES);
725    }
726}
727