1/*
2 * Copyright (C) 2009 The Android Open Source Project
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 */
16
17package com.android.inputmethod.pinyin;
18
19import android.content.Context;
20import android.graphics.Canvas;
21import android.graphics.Paint;
22import android.graphics.Rect;
23import android.graphics.Paint.FontMetricsInt;
24import android.graphics.drawable.ColorDrawable;
25import android.graphics.drawable.Drawable;
26import android.os.Handler;
27import android.view.Gravity;
28import android.view.View;
29import android.view.View.MeasureSpec;
30import android.widget.PopupWindow;
31
32/**
33 * Subclass of PopupWindow used as the feedback when user presses on a soft key
34 * or a candidate.
35 */
36public class BalloonHint extends PopupWindow {
37    /**
38     * Delayed time to show the balloon hint.
39     */
40    public static final int TIME_DELAY_SHOW = 0;
41
42    /**
43     * Delayed time to dismiss the balloon hint.
44     */
45    public static final int TIME_DELAY_DISMISS = 200;
46
47    /**
48     * The padding information of the balloon. Because PopupWindow's background
49     * can not be changed unless it is dismissed and shown again, we set the
50     * real background drawable to the content view, and make the PopupWindow's
51     * background transparent. So actually this padding information is for the
52     * content view.
53     */
54    private Rect mPaddingRect = new Rect();
55
56    /**
57     * The context used to create this balloon hint object.
58     */
59    private Context mContext;
60
61    /**
62     * Parent used to show the balloon window.
63     */
64    private View mParent;
65
66    /**
67     * The content view of the balloon.
68     */
69    BalloonView mBalloonView;
70
71    /**
72     * The measuring specification used to determine its size. Key-press
73     * balloons and candidates balloons have different measuring specifications.
74     */
75    private int mMeasureSpecMode;
76
77    /**
78     * Used to indicate whether the balloon needs to be dismissed forcibly.
79     */
80    private boolean mForceDismiss;
81
82    /**
83     * Timer used to show/dismiss the balloon window with some time delay.
84     */
85    private BalloonTimer mBalloonTimer;
86
87    private int mParentLocationInWindow[] = new int[2];
88
89    public BalloonHint(Context context, View parent, int measureSpecMode) {
90        super(context);
91        mParent = parent;
92        mMeasureSpecMode = measureSpecMode;
93
94        setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
95        setTouchable(false);
96        setBackgroundDrawable(new ColorDrawable(0));
97
98        mBalloonView = new BalloonView(context);
99        mBalloonView.setClickable(false);
100        setContentView(mBalloonView);
101
102        mBalloonTimer = new BalloonTimer();
103    }
104
105    public Context getContext() {
106        return mContext;
107    }
108
109    public Rect getPadding() {
110        return mPaddingRect;
111    }
112
113    public void setBalloonBackground(Drawable drawable) {
114        // We usually pick up a background from a soft keyboard template,
115        // and the object may has been set to this balloon before.
116        if (mBalloonView.getBackground() == drawable) return;
117        mBalloonView.setBackgroundDrawable(drawable);
118
119        if (null != drawable) {
120            drawable.getPadding(mPaddingRect);
121        } else {
122            mPaddingRect.set(0, 0, 0, 0);
123        }
124    }
125
126    /**
127     * Set configurations to show text label in this balloon.
128     *
129     * @param label The text label to show in the balloon.
130     * @param textSize The text size used to show label.
131     * @param textBold Used to indicate whether the label should be bold.
132     * @param textColor The text color used to show label.
133     * @param width The desired width of the balloon. The real width is
134     *        determined by the desired width and balloon's measuring
135     *        specification.
136     * @param height The desired width of the balloon. The real width is
137     *        determined by the desired width and balloon's measuring
138     *        specification.
139     */
140    public void setBalloonConfig(String label, float textSize,
141            boolean textBold, int textColor, int width, int height) {
142        mBalloonView.setTextConfig(label, textSize, textBold, textColor);
143        setBalloonSize(width, height);
144    }
145
146    /**
147     * Set configurations to show text label in this balloon.
148     *
149     * @param icon The icon used to shown in this balloon.
150     * @param width The desired width of the balloon. The real width is
151     *        determined by the desired width and balloon's measuring
152     *        specification.
153     * @param height The desired width of the balloon. The real width is
154     *        determined by the desired width and balloon's measuring
155     *        specification.
156     */
157    public void setBalloonConfig(Drawable icon, int width, int height) {
158        mBalloonView.setIcon(icon);
159        setBalloonSize(width, height);
160    }
161
162
163    public boolean needForceDismiss() {
164        return mForceDismiss;
165    }
166
167    public int getPaddingLeft() {
168        return mPaddingRect.left;
169    }
170
171    public int getPaddingTop() {
172        return mPaddingRect.top;
173    }
174
175    public int getPaddingRight() {
176        return mPaddingRect.right;
177    }
178
179    public int getPaddingBottom() {
180        return mPaddingRect.bottom;
181    }
182
183    public void delayedShow(long delay, int locationInParent[]) {
184        if (mBalloonTimer.isPending()) {
185            mBalloonTimer.removeTimer();
186        }
187        if (delay <= 0) {
188            mParent.getLocationInWindow(mParentLocationInWindow);
189            showAtLocation(mParent, Gravity.LEFT | Gravity.TOP,
190                    locationInParent[0], locationInParent[1]
191                            + mParentLocationInWindow[1]);
192        } else {
193            mBalloonTimer.startTimer(delay, BalloonTimer.ACTION_SHOW,
194                    locationInParent, -1, -1);
195        }
196    }
197
198    public void delayedUpdate(long delay, int locationInParent[],
199            int width, int height) {
200        mBalloonView.invalidate();
201        if (mBalloonTimer.isPending()) {
202            mBalloonTimer.removeTimer();
203        }
204        if (delay <= 0) {
205            mParent.getLocationInWindow(mParentLocationInWindow);
206            update(locationInParent[0], locationInParent[1]
207                    + mParentLocationInWindow[1], width, height);
208        } else {
209            mBalloonTimer.startTimer(delay, BalloonTimer.ACTION_UPDATE,
210                    locationInParent, width, height);
211        }
212    }
213
214    public void delayedDismiss(long delay) {
215        if (mBalloonTimer.isPending()) {
216            mBalloonTimer.removeTimer();
217            int pendingAction = mBalloonTimer.getAction();
218            if (0 != delay && BalloonTimer.ACTION_HIDE != pendingAction) {
219                mBalloonTimer.run();
220            }
221        }
222        if (delay <= 0) {
223            dismiss();
224        } else {
225            mBalloonTimer.startTimer(delay, BalloonTimer.ACTION_HIDE, null, -1,
226                    -1);
227        }
228    }
229
230    public void removeTimer() {
231        if (mBalloonTimer.isPending()) {
232            mBalloonTimer.removeTimer();
233        }
234    }
235
236    private void setBalloonSize(int width, int height) {
237        int widthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
238                mMeasureSpecMode);
239        int heightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
240                mMeasureSpecMode);
241        mBalloonView.measure(widthMeasureSpec, heightMeasureSpec);
242
243        int oldWidth = getWidth();
244        int oldHeight = getHeight();
245        int newWidth = mBalloonView.getMeasuredWidth() + getPaddingLeft()
246                + getPaddingRight();
247        int newHeight = mBalloonView.getMeasuredHeight() + getPaddingTop()
248                + getPaddingBottom();
249        setWidth(newWidth);
250        setHeight(newHeight);
251
252        // If update() is called to update both size and position, the system
253        // will first MOVE the PopupWindow to the new position, and then
254        // perform a size-updating operation, so there will be a flash in
255        // PopupWindow if user presses a key and moves finger to next one whose
256        // size is different.
257        // PopupWindow will handle the updating issue in one go in the future,
258        // but before that, if we find the size is changed, a mandatory dismiss
259        // operation is required. In our UI design, normal QWERTY keys' width
260        // can be different in 1-pixel, and we do not dismiss the balloon when
261        // user move between QWERTY keys.
262        mForceDismiss = false;
263        if (isShowing()) {
264            mForceDismiss = oldWidth - newWidth > 1 || newWidth - oldWidth > 1;
265        }
266    }
267
268
269    private class BalloonTimer extends Handler implements Runnable {
270        public static final int ACTION_SHOW = 1;
271        public static final int ACTION_HIDE = 2;
272        public static final int ACTION_UPDATE = 3;
273
274        /**
275         * The pending action.
276         */
277        private int mAction;
278
279        private int mPositionInParent[] = new int[2];
280        private int mWidth;
281        private int mHeight;
282
283        private boolean mTimerPending = false;
284
285        public void startTimer(long time, int action, int positionInParent[],
286                int width, int height) {
287            mAction = action;
288            if (ACTION_HIDE != action) {
289                mPositionInParent[0] = positionInParent[0];
290                mPositionInParent[1] = positionInParent[1];
291            }
292            mWidth = width;
293            mHeight = height;
294            postDelayed(this, time);
295            mTimerPending = true;
296        }
297
298        public boolean isPending() {
299            return mTimerPending;
300        }
301
302        public boolean removeTimer() {
303            if (mTimerPending) {
304                mTimerPending = false;
305                removeCallbacks(this);
306                return true;
307            }
308
309            return false;
310        }
311
312        public int getAction() {
313            return mAction;
314        }
315
316        public void run() {
317            switch (mAction) {
318            case ACTION_SHOW:
319                mParent.getLocationInWindow(mParentLocationInWindow);
320                showAtLocation(mParent, Gravity.LEFT | Gravity.TOP,
321                        mPositionInParent[0], mPositionInParent[1]
322                                + mParentLocationInWindow[1]);
323                break;
324            case ACTION_HIDE:
325                dismiss();
326                break;
327            case ACTION_UPDATE:
328                mParent.getLocationInWindow(mParentLocationInWindow);
329                update(mPositionInParent[0], mPositionInParent[1]
330                        + mParentLocationInWindow[1], mWidth, mHeight);
331            }
332            mTimerPending = false;
333        }
334    }
335
336    private class BalloonView extends View {
337        /**
338         * Suspension points used to display long items.
339         */
340        private static final String SUSPENSION_POINTS = "...";
341
342        /**
343         * The icon to be shown. If it is not null, {@link #mLabel} will be
344         * ignored.
345         */
346        private Drawable mIcon;
347
348        /**
349         * The label to be shown. It is enabled only if {@link #mIcon} is null.
350         */
351        private String mLabel;
352
353        private int mLabeColor = 0xff000000;
354        private Paint mPaintLabel;
355        private FontMetricsInt mFmi;
356
357        /**
358         * The width to show suspension points.
359         */
360        private float mSuspensionPointsWidth;
361
362
363        public BalloonView(Context context) {
364            super(context);
365            mPaintLabel = new Paint();
366            mPaintLabel.setColor(mLabeColor);
367            mPaintLabel.setAntiAlias(true);
368            mPaintLabel.setFakeBoldText(true);
369            mFmi = mPaintLabel.getFontMetricsInt();
370        }
371
372        public void setIcon(Drawable icon) {
373            mIcon = icon;
374        }
375
376        public void setTextConfig(String label, float fontSize,
377                boolean textBold, int textColor) {
378            // Icon should be cleared so that the label will be enabled.
379            mIcon = null;
380            mLabel = label;
381            mPaintLabel.setTextSize(fontSize);
382            mPaintLabel.setFakeBoldText(textBold);
383            mPaintLabel.setColor(textColor);
384            mFmi = mPaintLabel.getFontMetricsInt();
385            mSuspensionPointsWidth = mPaintLabel.measureText(SUSPENSION_POINTS);
386        }
387
388        @Override
389        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
390            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
391            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
392            final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
393            final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
394
395            if (widthMode == MeasureSpec.EXACTLY) {
396                setMeasuredDimension(widthSize, heightSize);
397                return;
398            }
399
400            int measuredWidth = mPaddingLeft + mPaddingRight;
401            int measuredHeight = mPaddingTop + mPaddingBottom;
402            if (null != mIcon) {
403                measuredWidth += mIcon.getIntrinsicWidth();
404                measuredHeight += mIcon.getIntrinsicHeight();
405            } else if (null != mLabel) {
406                measuredWidth += (int) (mPaintLabel.measureText(mLabel));
407                measuredHeight += mFmi.bottom - mFmi.top;
408            }
409            if (widthSize > measuredWidth || widthMode == MeasureSpec.AT_MOST) {
410                measuredWidth = widthSize;
411            }
412
413            if (heightSize > measuredHeight
414                    || heightMode == MeasureSpec.AT_MOST) {
415                measuredHeight = heightSize;
416            }
417
418            int maxWidth = Environment.getInstance().getScreenWidth() -
419                    mPaddingLeft - mPaddingRight;
420            if (measuredWidth > maxWidth) {
421                measuredWidth = maxWidth;
422            }
423            setMeasuredDimension(measuredWidth, measuredHeight);
424        }
425
426        @Override
427        protected void onDraw(Canvas canvas) {
428            int width = getWidth();
429            int height = getHeight();
430            if (null != mIcon) {
431                int marginLeft = (width - mIcon.getIntrinsicWidth()) / 2;
432                int marginRight = width - mIcon.getIntrinsicWidth()
433                        - marginLeft;
434                int marginTop = (height - mIcon.getIntrinsicHeight()) / 2;
435                int marginBottom = height - mIcon.getIntrinsicHeight()
436                        - marginTop;
437                mIcon.setBounds(marginLeft, marginTop, width - marginRight,
438                        height - marginBottom);
439                mIcon.draw(canvas);
440            } else if (null != mLabel) {
441                float labelMeasuredWidth = mPaintLabel.measureText(mLabel);
442                float x = mPaddingLeft;
443                x += (width - labelMeasuredWidth - mPaddingLeft - mPaddingRight) / 2.0f;
444                String labelToDraw = mLabel;
445                if (x < mPaddingLeft) {
446                    x = mPaddingLeft;
447                    labelToDraw = getLimitedLabelForDrawing(mLabel,
448                            width - mPaddingLeft - mPaddingRight);
449                }
450
451                int fontHeight = mFmi.bottom - mFmi.top;
452                float marginY = (height - fontHeight) / 2.0f;
453                float y = marginY - mFmi.top;
454                canvas.drawText(labelToDraw, x, y, mPaintLabel);
455            }
456        }
457
458        private String getLimitedLabelForDrawing(String rawLabel,
459                float widthToDraw) {
460            int subLen = rawLabel.length();
461            if (subLen <= 1) return rawLabel;
462            do {
463                subLen--;
464                float width = mPaintLabel.measureText(rawLabel, 0, subLen);
465                if (width + mSuspensionPointsWidth <= widthToDraw || 1 >= subLen) {
466                    return rawLabel.substring(0, subLen) +
467                            SUSPENSION_POINTS;
468                }
469            } while (true);
470        }
471    }
472}
473