1/*
2 * Copyright (C) 2010 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.keyboard;
18
19import static com.android.inputmethod.keyboard.internal.KeyboardIconsSet.ICON_UNDEFINED;
20import static com.android.inputmethod.latin.Constants.CODE_OUTPUT_TEXT;
21import static com.android.inputmethod.latin.Constants.CODE_SHIFT;
22import static com.android.inputmethod.latin.Constants.CODE_SWITCH_ALPHA_SYMBOL;
23import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED;
24
25import android.content.res.Resources;
26import android.content.res.TypedArray;
27import android.graphics.Rect;
28import android.graphics.Typeface;
29import android.graphics.drawable.Drawable;
30import android.text.TextUtils;
31import android.util.Log;
32import android.util.Xml;
33
34import com.android.inputmethod.keyboard.internal.KeyDrawParams;
35import com.android.inputmethod.keyboard.internal.KeySpecParser;
36import com.android.inputmethod.keyboard.internal.KeyStyle;
37import com.android.inputmethod.keyboard.internal.KeyVisualAttributes;
38import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
39import com.android.inputmethod.keyboard.internal.KeyboardParams;
40import com.android.inputmethod.keyboard.internal.KeyboardRow;
41import com.android.inputmethod.keyboard.internal.MoreKeySpec;
42import com.android.inputmethod.latin.Constants;
43import com.android.inputmethod.latin.R;
44import com.android.inputmethod.latin.utils.StringUtils;
45
46import org.xmlpull.v1.XmlPullParser;
47import org.xmlpull.v1.XmlPullParserException;
48
49import java.util.Arrays;
50import java.util.Locale;
51
52/**
53 * Class for describing the position and characteristics of a single key in the keyboard.
54 */
55public class Key implements Comparable<Key> {
56    private static final String TAG = Key.class.getSimpleName();
57
58    /**
59     * The key code (unicode or custom code) that this key generates.
60     */
61    private final int mCode;
62
63    /** Label to display */
64    private final String mLabel;
65    /** Hint label to display on the key in conjunction with the label */
66    private final String mHintLabel;
67    /** Flags of the label */
68    private final int mLabelFlags;
69    private static final int LABEL_FLAGS_ALIGN_LEFT = 0x01;
70    private static final int LABEL_FLAGS_ALIGN_RIGHT = 0x02;
71    private static final int LABEL_FLAGS_ALIGN_LEFT_OF_CENTER = 0x08;
72    private static final int LABEL_FLAGS_FONT_NORMAL = 0x10;
73    private static final int LABEL_FLAGS_FONT_MONO_SPACE = 0x20;
74    // Start of key text ratio enum values
75    private static final int LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK = 0x1C0;
76    private static final int LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO = 0x40;
77    private static final int LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO = 0x80;
78    private static final int LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO = 0xC0;
79    private static final int LABEL_FLAGS_FOLLOW_KEY_LARGE_LABEL_RATIO = 0x100;
80    private static final int LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO = 0x140;
81    // End of key text ratio mask enum values
82    private static final int LABEL_FLAGS_HAS_POPUP_HINT = 0x200;
83    private static final int LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT = 0x400;
84    private static final int LABEL_FLAGS_HAS_HINT_LABEL = 0x800;
85    private static final int LABEL_FLAGS_WITH_ICON_LEFT = 0x1000;
86    private static final int LABEL_FLAGS_WITH_ICON_RIGHT = 0x2000;
87    private static final int LABEL_FLAGS_AUTO_X_SCALE = 0x4000;
88    private static final int LABEL_FLAGS_PRESERVE_CASE = 0x8000;
89    private static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x10000;
90    private static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x20000;
91    private static final int LABEL_FLAGS_DISABLE_HINT_LABEL = 0x40000000;
92    private static final int LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS = 0x80000000;
93
94    /** Icon to display instead of a label. Icon takes precedence over a label */
95    private final int mIconId;
96
97    /** Width of the key, not including the gap */
98    private final int mWidth;
99    /** Height of the key, not including the gap */
100    private final int mHeight;
101    /** X coordinate of the key in the keyboard layout */
102    private final int mX;
103    /** Y coordinate of the key in the keyboard layout */
104    private final int mY;
105    /** Hit bounding box of the key */
106    private final Rect mHitBox = new Rect();
107
108    /** More keys. It is guaranteed that this is null or an array of one or more elements */
109    private final MoreKeySpec[] mMoreKeys;
110    /** More keys column number and flags */
111    private final int mMoreKeysColumnAndFlags;
112    private static final int MORE_KEYS_COLUMN_MASK = 0x000000ff;
113    private static final int MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER = 0x80000000;
114    private static final int MORE_KEYS_FLAGS_HAS_LABELS = 0x40000000;
115    private static final int MORE_KEYS_FLAGS_NEEDS_DIVIDERS = 0x20000000;
116    private static final int MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY = 0x10000000;
117    private static final String MORE_KEYS_AUTO_COLUMN_ORDER = "!autoColumnOrder!";
118    private static final String MORE_KEYS_FIXED_COLUMN_ORDER = "!fixedColumnOrder!";
119    private static final String MORE_KEYS_HAS_LABELS = "!hasLabels!";
120    private static final String MORE_KEYS_NEEDS_DIVIDERS = "!needsDividers!";
121    private static final String MORE_KEYS_NO_PANEL_AUTO_MORE_KEY = "!noPanelAutoMoreKey!";
122
123    /** Background type that represents different key background visual than normal one. */
124    private final int mBackgroundType;
125    public static final int BACKGROUND_TYPE_EMPTY = 0;
126    public static final int BACKGROUND_TYPE_NORMAL = 1;
127    public static final int BACKGROUND_TYPE_FUNCTIONAL = 2;
128    public static final int BACKGROUND_TYPE_ACTION = 3;
129    public static final int BACKGROUND_TYPE_STICKY_OFF = 4;
130    public static final int BACKGROUND_TYPE_STICKY_ON = 5;
131
132    private final int mActionFlags;
133    private static final int ACTION_FLAGS_IS_REPEATABLE = 0x01;
134    private static final int ACTION_FLAGS_NO_KEY_PREVIEW = 0x02;
135    private static final int ACTION_FLAGS_ALT_CODE_WHILE_TYPING = 0x04;
136    private static final int ACTION_FLAGS_ENABLE_LONG_PRESS = 0x08;
137
138    private final KeyVisualAttributes mKeyVisualAttributes;
139
140    private final OptionalAttributes mOptionalAttributes;
141
142    private static final int DEFAULT_TEXT_COLOR = 0xFFFFFFFF;
143
144    private static final class OptionalAttributes {
145        /** Text to output when pressed. This can be multiple characters, like ".com" */
146        public final String mOutputText;
147        public final int mAltCode;
148        /** Icon for disabled state */
149        public final int mDisabledIconId;
150        /** Preview version of the icon, for the preview popup */
151        public final int mPreviewIconId;
152        /** The visual insets */
153        public final int mVisualInsetsLeft;
154        public final int mVisualInsetsRight;
155
156        private OptionalAttributes(final String outputText, final int altCode,
157                final int disabledIconId, final int previewIconId,
158                final int visualInsetsLeft, final int visualInsetsRight) {
159            mOutputText = outputText;
160            mAltCode = altCode;
161            mDisabledIconId = disabledIconId;
162            mPreviewIconId = previewIconId;
163            mVisualInsetsLeft = visualInsetsLeft;
164            mVisualInsetsRight = visualInsetsRight;
165        }
166
167        public static OptionalAttributes newInstance(final String outputText, final int altCode,
168                final int disabledIconId, final int previewIconId,
169                final int visualInsetsLeft, final int visualInsetsRight) {
170            if (outputText == null && altCode == CODE_UNSPECIFIED
171                    && disabledIconId == ICON_UNDEFINED && previewIconId == ICON_UNDEFINED
172                    && visualInsetsLeft == 0 && visualInsetsRight == 0) {
173                return null;
174            }
175            return new OptionalAttributes(outputText, altCode, disabledIconId, previewIconId,
176                    visualInsetsLeft, visualInsetsRight);
177        }
178    }
179
180    private final int mHashCode;
181
182    /** The current pressed state of this key */
183    private boolean mPressed;
184    /** Key is enabled and responds on press */
185    private boolean mEnabled = true;
186
187    /**
188     * This constructor is being used only for keys in more keys keyboard.
189     */
190    public Key(final KeyboardParams params, final MoreKeySpec moreKeySpec, final int x, final int y,
191            final int width, final int height, final int labelFlags) {
192        this(params, moreKeySpec.mLabel, null, moreKeySpec.mIconId, moreKeySpec.mCode,
193                moreKeySpec.mOutputText, x, y, width, height, labelFlags, BACKGROUND_TYPE_NORMAL);
194    }
195
196    /**
197     * This constructor is being used only for key in popup suggestions pane.
198     */
199    public Key(final KeyboardParams params, final String label, final String hintLabel,
200            final int iconId, final int code, final String outputText, final int x, final int y,
201            final int width, final int height, final int labelFlags, final int backgroundType) {
202        mHeight = height - params.mVerticalGap;
203        mWidth = width - params.mHorizontalGap;
204        mHintLabel = hintLabel;
205        mLabelFlags = labelFlags;
206        mBackgroundType = backgroundType;
207        mActionFlags = 0;
208        mMoreKeys = null;
209        mMoreKeysColumnAndFlags = 0;
210        mLabel = label;
211        mOptionalAttributes = OptionalAttributes.newInstance(outputText, CODE_UNSPECIFIED,
212                ICON_UNDEFINED, ICON_UNDEFINED,
213                0 /* visualInsetsLeft */, 0 /* visualInsetsRight */);
214        mCode = code;
215        mEnabled = (code != CODE_UNSPECIFIED);
216        mIconId = iconId;
217        // Horizontal gap is divided equally to both sides of the key.
218        mX = x + params.mHorizontalGap / 2;
219        mY = y;
220        mHitBox.set(x, y, x + width + 1, y + height);
221        mKeyVisualAttributes = null;
222
223        mHashCode = computeHashCode(this);
224    }
225
226    /**
227     * Create a key with the given top-left coordinate and extract its attributes from the XML
228     * parser.
229     * @param res resources associated with the caller's context
230     * @param params the keyboard building parameters.
231     * @param row the row that this key belongs to. row's x-coordinate will be the right edge of
232     *        this key.
233     * @param parser the XML parser containing the attributes for this key
234     * @throws XmlPullParserException
235     */
236    public Key(final Resources res, final KeyboardParams params, final KeyboardRow row,
237            final XmlPullParser parser) throws XmlPullParserException {
238        final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap;
239        final int rowHeight = row.getRowHeight();
240        mHeight = rowHeight - params.mVerticalGap;
241
242        final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
243                R.styleable.Keyboard_Key);
244
245        final KeyStyle style = params.mKeyStyles.getKeyStyle(keyAttr, parser);
246        final float keyXPos = row.getKeyX(keyAttr);
247        final float keyWidth = row.getKeyWidth(keyAttr, keyXPos);
248        final int keyYPos = row.getKeyY();
249
250        // Horizontal gap is divided equally to both sides of the key.
251        mX = Math.round(keyXPos + horizontalGap / 2);
252        mY = keyYPos;
253        mWidth = Math.round(keyWidth - horizontalGap);
254        mHitBox.set(Math.round(keyXPos), keyYPos, Math.round(keyXPos + keyWidth) + 1,
255                keyYPos + rowHeight);
256        // Update row to have current x coordinate.
257        row.setXPos(keyXPos + keyWidth);
258
259        mBackgroundType = style.getInt(keyAttr,
260                R.styleable.Keyboard_Key_backgroundType, row.getDefaultBackgroundType());
261
262        final int baseWidth = params.mBaseWidth;
263        final int visualInsetsLeft = Math.round(keyAttr.getFraction(
264                R.styleable.Keyboard_Key_visualInsetsLeft, baseWidth, baseWidth, 0));
265        final int visualInsetsRight = Math.round(keyAttr.getFraction(
266                R.styleable.Keyboard_Key_visualInsetsRight, baseWidth, baseWidth, 0));
267        mIconId = KeySpecParser.getIconId(style.getString(keyAttr,
268                R.styleable.Keyboard_Key_keyIcon));
269        final int disabledIconId = KeySpecParser.getIconId(style.getString(keyAttr,
270                R.styleable.Keyboard_Key_keyIconDisabled));
271        final int previewIconId = KeySpecParser.getIconId(style.getString(keyAttr,
272                R.styleable.Keyboard_Key_keyIconPreview));
273
274        mLabelFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags)
275                | row.getDefaultKeyLabelFlags();
276        final boolean needsToUpperCase = needsToUpperCase(mLabelFlags, params.mId.mElementId);
277        final Locale locale = params.mId.mLocale;
278        int actionFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
279        String[] moreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
280
281        int moreKeysColumn = style.getInt(keyAttr,
282                R.styleable.Keyboard_Key_maxMoreKeysColumn, params.mMaxMoreKeysKeyboardColumn);
283        int value;
284        if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) {
285            moreKeysColumn = value & MORE_KEYS_COLUMN_MASK;
286        }
287        if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) {
288            moreKeysColumn = MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER | (value & MORE_KEYS_COLUMN_MASK);
289        }
290        if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) {
291            moreKeysColumn |= MORE_KEYS_FLAGS_HAS_LABELS;
292        }
293        if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) {
294            moreKeysColumn |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS;
295        }
296        if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) {
297            moreKeysColumn |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY;
298        }
299        mMoreKeysColumnAndFlags = moreKeysColumn;
300
301        final String[] additionalMoreKeys;
302        if ((mLabelFlags & LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS) != 0) {
303            additionalMoreKeys = null;
304        } else {
305            additionalMoreKeys = style.getStringArray(keyAttr,
306                    R.styleable.Keyboard_Key_additionalMoreKeys);
307        }
308        moreKeys = KeySpecParser.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys);
309        if (moreKeys != null) {
310            actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
311            mMoreKeys = new MoreKeySpec[moreKeys.length];
312            for (int i = 0; i < moreKeys.length; i++) {
313                mMoreKeys[i] = new MoreKeySpec(
314                        moreKeys[i], needsToUpperCase, locale, params.mCodesSet);
315            }
316        } else {
317            mMoreKeys = null;
318        }
319        mActionFlags = actionFlags;
320
321        final int code = KeySpecParser.parseCode(style.getString(keyAttr,
322                R.styleable.Keyboard_Key_code), params.mCodesSet, CODE_UNSPECIFIED);
323        if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) {
324            mLabel = params.mId.mCustomActionLabel;
325        } else if (code >= Character.MIN_SUPPLEMENTARY_CODE_POINT) {
326            // This is a workaround to have a key that has a supplementary code point in its label.
327            // Because we can put a string in resource neither as a XML entity of a supplementary
328            // code point nor as a surrogate pair.
329            mLabel = new StringBuilder().appendCodePoint(code).toString();
330        } else {
331            mLabel = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr,
332                    R.styleable.Keyboard_Key_keyLabel), needsToUpperCase, locale);
333        }
334        if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) {
335            mHintLabel = null;
336        } else {
337            mHintLabel = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr,
338                    R.styleable.Keyboard_Key_keyHintLabel), needsToUpperCase, locale);
339        }
340        String outputText = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr,
341                R.styleable.Keyboard_Key_keyOutputText), needsToUpperCase, locale);
342        // Choose the first letter of the label as primary code if not specified.
343        if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText)
344                && !TextUtils.isEmpty(mLabel)) {
345            if (StringUtils.codePointCount(mLabel) == 1) {
346                // Use the first letter of the hint label if shiftedLetterActivated flag is
347                // specified.
348                if (hasShiftedLetterHint() && isShiftedLetterActivated()
349                        && !TextUtils.isEmpty(mHintLabel)) {
350                    mCode = mHintLabel.codePointAt(0);
351                } else {
352                    mCode = mLabel.codePointAt(0);
353                }
354            } else {
355                // In some locale and case, the character might be represented by multiple code
356                // points, such as upper case Eszett of German alphabet.
357                outputText = mLabel;
358                mCode = CODE_OUTPUT_TEXT;
359            }
360        } else if (code == CODE_UNSPECIFIED && outputText != null) {
361            if (StringUtils.codePointCount(outputText) == 1) {
362                mCode = outputText.codePointAt(0);
363                outputText = null;
364            } else {
365                mCode = CODE_OUTPUT_TEXT;
366            }
367        } else {
368            mCode = KeySpecParser.toUpperCaseOfCodeForLocale(code, needsToUpperCase, locale);
369        }
370        final int altCode = KeySpecParser.toUpperCaseOfCodeForLocale(
371                KeySpecParser.parseCode(style.getString(keyAttr,
372                R.styleable.Keyboard_Key_altCode), params.mCodesSet, CODE_UNSPECIFIED),
373                needsToUpperCase, locale);
374        mOptionalAttributes = OptionalAttributes.newInstance(outputText, altCode,
375                disabledIconId, previewIconId, visualInsetsLeft, visualInsetsRight);
376        mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
377        keyAttr.recycle();
378        mHashCode = computeHashCode(this);
379        if (hasShiftedLetterHint() && TextUtils.isEmpty(mHintLabel)) {
380            Log.w(TAG, "hasShiftedLetterHint specified without keyHintLabel: " + this);
381        }
382    }
383
384    /**
385     * Copy constructor.
386     *
387     * @param key the original key.
388     */
389    protected Key(final Key key) {
390        // Final attributes.
391        mCode = key.mCode;
392        mLabel = key.mLabel;
393        mHintLabel = key.mHintLabel;
394        mLabelFlags = key.mLabelFlags;
395        mIconId = key.mIconId;
396        mWidth = key.mWidth;
397        mHeight = key.mHeight;
398        mX = key.mX;
399        mY = key.mY;
400        mHitBox.set(key.mHitBox);
401        mMoreKeys = key.mMoreKeys;
402        mMoreKeysColumnAndFlags = key.mMoreKeysColumnAndFlags;
403        mBackgroundType = key.mBackgroundType;
404        mActionFlags = key.mActionFlags;
405        mKeyVisualAttributes = key.mKeyVisualAttributes;
406        mOptionalAttributes = key.mOptionalAttributes;
407        mHashCode = key.mHashCode;
408        // Key state.
409        mPressed = key.mPressed;
410        mEnabled = key.mEnabled;
411    }
412
413    private static boolean needsToUpperCase(final int labelFlags, final int keyboardElementId) {
414        if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false;
415        switch (keyboardElementId) {
416        case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
417        case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
418        case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
419        case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
420            return true;
421        default:
422            return false;
423        }
424    }
425
426    private static int computeHashCode(final Key key) {
427        return Arrays.hashCode(new Object[] {
428                key.mX,
429                key.mY,
430                key.mWidth,
431                key.mHeight,
432                key.mCode,
433                key.mLabel,
434                key.mHintLabel,
435                key.mIconId,
436                key.mBackgroundType,
437                Arrays.hashCode(key.mMoreKeys),
438                key.getOutputText(),
439                key.mActionFlags,
440                key.mLabelFlags,
441                // Key can be distinguishable without the following members.
442                // key.mOptionalAttributes.mAltCode,
443                // key.mOptionalAttributes.mDisabledIconId,
444                // key.mOptionalAttributes.mPreviewIconId,
445                // key.mHorizontalGap,
446                // key.mVerticalGap,
447                // key.mOptionalAttributes.mVisualInsetLeft,
448                // key.mOptionalAttributes.mVisualInsetRight,
449                // key.mMaxMoreKeysColumn,
450        });
451    }
452
453    private boolean equalsInternal(final Key o) {
454        if (this == o) return true;
455        return o.mX == mX
456                && o.mY == mY
457                && o.mWidth == mWidth
458                && o.mHeight == mHeight
459                && o.mCode == mCode
460                && TextUtils.equals(o.mLabel, mLabel)
461                && TextUtils.equals(o.mHintLabel, mHintLabel)
462                && o.mIconId == mIconId
463                && o.mBackgroundType == mBackgroundType
464                && Arrays.equals(o.mMoreKeys, mMoreKeys)
465                && TextUtils.equals(o.getOutputText(), getOutputText())
466                && o.mActionFlags == mActionFlags
467                && o.mLabelFlags == mLabelFlags;
468    }
469
470    @Override
471    public int compareTo(Key o) {
472        if (equalsInternal(o)) return 0;
473        if (mHashCode > o.mHashCode) return 1;
474        return -1;
475    }
476
477    @Override
478    public int hashCode() {
479        return mHashCode;
480    }
481
482    @Override
483    public boolean equals(final Object o) {
484        return o instanceof Key && equalsInternal((Key)o);
485    }
486
487    @Override
488    public String toString() {
489        final String label;
490        if (StringUtils.codePointCount(mLabel) == 1 && mLabel.codePointAt(0) == mCode) {
491            label = "";
492        } else {
493            label = "/" + mLabel;
494        }
495        return String.format(Locale.ROOT, "%s%s %d,%d %dx%d %s/%s/%s",
496                Constants.printableCode(mCode), label, mX, mY, mWidth, mHeight, mHintLabel,
497                KeyboardIconsSet.getIconName(mIconId), backgroundName(mBackgroundType));
498    }
499
500    private static String backgroundName(final int backgroundType) {
501        switch (backgroundType) {
502        case BACKGROUND_TYPE_EMPTY: return "empty";
503        case BACKGROUND_TYPE_NORMAL: return "normal";
504        case BACKGROUND_TYPE_FUNCTIONAL: return "functional";
505        case BACKGROUND_TYPE_ACTION: return "action";
506        case BACKGROUND_TYPE_STICKY_OFF: return "stickyOff";
507        case BACKGROUND_TYPE_STICKY_ON: return "stickyOn";
508        default: return null;
509        }
510    }
511
512    public int getCode() {
513        return mCode;
514    }
515
516    public String getLabel() {
517        return mLabel;
518    }
519
520    public String getHintLabel() {
521        return mHintLabel;
522    }
523
524    public MoreKeySpec[] getMoreKeys() {
525        return mMoreKeys;
526    }
527
528    public void markAsLeftEdge(final KeyboardParams params) {
529        mHitBox.left = params.mLeftPadding;
530    }
531
532    public void markAsRightEdge(final KeyboardParams params) {
533        mHitBox.right = params.mOccupiedWidth - params.mRightPadding;
534    }
535
536    public void markAsTopEdge(final KeyboardParams params) {
537        mHitBox.top = params.mTopPadding;
538    }
539
540    public void markAsBottomEdge(final KeyboardParams params) {
541        mHitBox.bottom = params.mOccupiedHeight + params.mBottomPadding;
542    }
543
544    public final boolean isSpacer() {
545        return this instanceof Spacer;
546    }
547
548    public final boolean isShift() {
549        return mCode == CODE_SHIFT;
550    }
551
552    public final boolean isModifier() {
553        return mCode == CODE_SHIFT || mCode == CODE_SWITCH_ALPHA_SYMBOL;
554    }
555
556    public final boolean isRepeatable() {
557        return (mActionFlags & ACTION_FLAGS_IS_REPEATABLE) != 0;
558    }
559
560    public final boolean noKeyPreview() {
561        return (mActionFlags & ACTION_FLAGS_NO_KEY_PREVIEW) != 0;
562    }
563
564    public final boolean altCodeWhileTyping() {
565        return (mActionFlags & ACTION_FLAGS_ALT_CODE_WHILE_TYPING) != 0;
566    }
567
568    public final boolean isLongPressEnabled() {
569        // We need not start long press timer on the key which has activated shifted letter.
570        return (mActionFlags & ACTION_FLAGS_ENABLE_LONG_PRESS) != 0
571                && (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) == 0;
572    }
573
574    public KeyVisualAttributes getVisualAttributes() {
575        return mKeyVisualAttributes;
576    }
577
578    public final Typeface selectTypeface(final KeyDrawParams params) {
579        // TODO: Handle "bold" here too?
580        if ((mLabelFlags & LABEL_FLAGS_FONT_NORMAL) != 0) {
581            return Typeface.DEFAULT;
582        }
583        if ((mLabelFlags & LABEL_FLAGS_FONT_MONO_SPACE) != 0) {
584            return Typeface.MONOSPACE;
585        }
586        return params.mTypeface;
587    }
588
589    public final int selectTextSize(final KeyDrawParams params) {
590        switch (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK) {
591        case LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO:
592            return params.mLetterSize;
593        case LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO:
594            return params.mLargeLetterSize;
595        case LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO:
596            return params.mLabelSize;
597        case LABEL_FLAGS_FOLLOW_KEY_LARGE_LABEL_RATIO:
598            return params.mLargeLabelSize;
599        case LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO:
600            return params.mHintLabelSize;
601        default: // No follow key ratio flag specified.
602            return StringUtils.codePointCount(mLabel) == 1 ? params.mLetterSize : params.mLabelSize;
603        }
604    }
605
606    public final int selectTextColor(final KeyDrawParams params) {
607        if (isShiftedLetterActivated()) {
608            return params.mTextInactivatedColor;
609        }
610        if (params.mTextColorStateList == null) {
611            return DEFAULT_TEXT_COLOR;
612        }
613        final int[] state;
614        // TODO: Hack!!!!!!!! Consider having a new attribute for the functional text labels.
615        // Currently, we distinguish "input key" from "functional key" by checking the
616        // length of the label( > 1) and "functional" attributes (= true).
617        if (mLabel != null && mLabel.length() > 1) {
618            state = getCurrentDrawableState();
619        } else {
620            state = KEY_STATE_NORMAL;
621        }
622        return params.mTextColorStateList.getColorForState(state, DEFAULT_TEXT_COLOR);
623    }
624
625    public final int selectHintTextSize(final KeyDrawParams params) {
626        if (hasHintLabel()) {
627            return params.mHintLabelSize;
628        }
629        if (hasShiftedLetterHint()) {
630            return params.mShiftedLetterHintSize;
631        }
632        return params.mHintLetterSize;
633    }
634
635    public final int selectHintTextColor(final KeyDrawParams params) {
636        if (hasHintLabel()) {
637            return params.mHintLabelColor;
638        }
639        if (hasShiftedLetterHint()) {
640            return isShiftedLetterActivated() ? params.mShiftedLetterHintActivatedColor
641                    : params.mShiftedLetterHintInactivatedColor;
642        }
643        return params.mHintLetterColor;
644    }
645
646    public final int selectMoreKeyTextSize(final KeyDrawParams params) {
647        return hasLabelsInMoreKeys() ? params.mLabelSize : params.mLetterSize;
648    }
649
650    public final String getPreviewLabel() {
651        return isShiftedLetterActivated() ? mHintLabel : mLabel;
652    }
653
654    private boolean previewHasLetterSize() {
655        return (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO) != 0
656                || StringUtils.codePointCount(getPreviewLabel()) == 1;
657    }
658
659    public final int selectPreviewTextSize(final KeyDrawParams params) {
660        if (previewHasLetterSize()) {
661            return params.mPreviewTextSize;
662        }
663        return params.mLetterSize;
664    }
665
666    public Typeface selectPreviewTypeface(final KeyDrawParams params) {
667        if (previewHasLetterSize()) {
668            return selectTypeface(params);
669        }
670        return Typeface.DEFAULT_BOLD;
671    }
672
673    public final boolean isAlignLeft() {
674        return (mLabelFlags & LABEL_FLAGS_ALIGN_LEFT) != 0;
675    }
676
677    public final boolean isAlignRight() {
678        return (mLabelFlags & LABEL_FLAGS_ALIGN_RIGHT) != 0;
679    }
680
681    public final boolean isAlignLeftOfCenter() {
682        return (mLabelFlags & LABEL_FLAGS_ALIGN_LEFT_OF_CENTER) != 0;
683    }
684
685    public final boolean hasPopupHint() {
686        return (mLabelFlags & LABEL_FLAGS_HAS_POPUP_HINT) != 0;
687    }
688
689    public final boolean hasShiftedLetterHint() {
690        return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0;
691    }
692
693    public final boolean hasHintLabel() {
694        return (mLabelFlags & LABEL_FLAGS_HAS_HINT_LABEL) != 0;
695    }
696
697    public final boolean hasLabelWithIconLeft() {
698        return (mLabelFlags & LABEL_FLAGS_WITH_ICON_LEFT) != 0;
699    }
700
701    public final boolean hasLabelWithIconRight() {
702        return (mLabelFlags & LABEL_FLAGS_WITH_ICON_RIGHT) != 0;
703    }
704
705    public final boolean needsXScale() {
706        return (mLabelFlags & LABEL_FLAGS_AUTO_X_SCALE) != 0;
707    }
708
709    public final boolean isShiftedLetterActivated() {
710        return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0;
711    }
712
713    public final int getMoreKeysColumn() {
714        return mMoreKeysColumnAndFlags & MORE_KEYS_COLUMN_MASK;
715    }
716
717    public final boolean isFixedColumnOrderMoreKeys() {
718        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER) != 0;
719    }
720
721    public final boolean hasLabelsInMoreKeys() {
722        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_HAS_LABELS) != 0;
723    }
724
725    public final int getMoreKeyLabelFlags() {
726        return hasLabelsInMoreKeys()
727                ? LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
728                : LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO;
729    }
730
731    public final boolean needsDividersInMoreKeys() {
732        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NEEDS_DIVIDERS) != 0;
733    }
734
735    public final boolean hasNoPanelAutoMoreKey() {
736        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY) != 0;
737    }
738
739    public final String getOutputText() {
740        final OptionalAttributes attrs = mOptionalAttributes;
741        return (attrs != null) ? attrs.mOutputText : null;
742    }
743
744    public final int getAltCode() {
745        final OptionalAttributes attrs = mOptionalAttributes;
746        return (attrs != null) ? attrs.mAltCode : CODE_UNSPECIFIED;
747    }
748
749    public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) {
750        final OptionalAttributes attrs = mOptionalAttributes;
751        final int disabledIconId = (attrs != null) ? attrs.mDisabledIconId : ICON_UNDEFINED;
752        final int iconId = mEnabled ? mIconId : disabledIconId;
753        final Drawable icon = iconSet.getIconDrawable(iconId);
754        if (icon != null) {
755            icon.setAlpha(alpha);
756        }
757        return icon;
758    }
759
760    public Drawable getPreviewIcon(final KeyboardIconsSet iconSet) {
761        final OptionalAttributes attrs = mOptionalAttributes;
762        final int previewIconId = (attrs != null) ? attrs.mPreviewIconId : ICON_UNDEFINED;
763        return previewIconId != ICON_UNDEFINED
764                ? iconSet.getIconDrawable(previewIconId) : iconSet.getIconDrawable(mIconId);
765    }
766
767    public int getWidth() {
768        return mWidth;
769    }
770
771    public int getHeight() {
772        return mHeight;
773    }
774
775    public int getX() {
776        return mX;
777    }
778
779    public int getY() {
780        return mY;
781    }
782
783    public final int getDrawX() {
784        final int x = getX();
785        final OptionalAttributes attrs = mOptionalAttributes;
786        return (attrs == null) ? x : x + attrs.mVisualInsetsLeft;
787    }
788
789    public final int getDrawWidth() {
790        final OptionalAttributes attrs = mOptionalAttributes;
791        return (attrs == null) ? mWidth
792                : mWidth - attrs.mVisualInsetsLeft - attrs.mVisualInsetsRight;
793    }
794
795    /**
796     * Informs the key that it has been pressed, in case it needs to change its appearance or
797     * state.
798     * @see #onReleased()
799     */
800    public void onPressed() {
801        mPressed = true;
802    }
803
804    /**
805     * Informs the key that it has been released, in case it needs to change its appearance or
806     * state.
807     * @see #onPressed()
808     */
809    public void onReleased() {
810        mPressed = false;
811    }
812
813    public final boolean isEnabled() {
814        return mEnabled;
815    }
816
817    public void setEnabled(final boolean enabled) {
818        mEnabled = enabled;
819    }
820
821    public Rect getHitBox() {
822        return mHitBox;
823    }
824
825    /**
826     * Detects if a point falls on this key.
827     * @param x the x-coordinate of the point
828     * @param y the y-coordinate of the point
829     * @return whether or not the point falls on the key. If the key is attached to an edge, it
830     * will assume that all points between the key and the edge are considered to be on the key.
831     * @see #markAsLeftEdge(KeyboardParams) etc.
832     */
833    public boolean isOnKey(final int x, final int y) {
834        return mHitBox.contains(x, y);
835    }
836
837    /**
838     * Returns the square of the distance to the nearest edge of the key and the given point.
839     * @param x the x-coordinate of the point
840     * @param y the y-coordinate of the point
841     * @return the square of the distance of the point from the nearest edge of the key
842     */
843    public int squaredDistanceToEdge(final int x, final int y) {
844        final int left = getX();
845        final int right = left + mWidth;
846        final int top = getY();
847        final int bottom = top + mHeight;
848        final int edgeX = x < left ? left : (x > right ? right : x);
849        final int edgeY = y < top ? top : (y > bottom ? bottom : y);
850        final int dx = x - edgeX;
851        final int dy = y - edgeY;
852        return dx * dx + dy * dy;
853    }
854
855    private final static int[] KEY_STATE_NORMAL_HIGHLIGHT_ON = {
856        android.R.attr.state_checkable,
857        android.R.attr.state_checked
858    };
859
860    private final static int[] KEY_STATE_PRESSED_HIGHLIGHT_ON = {
861        android.R.attr.state_pressed,
862        android.R.attr.state_checkable,
863        android.R.attr.state_checked
864    };
865
866    private final static int[] KEY_STATE_NORMAL_HIGHLIGHT_OFF = {
867        android.R.attr.state_checkable
868    };
869
870    private final static int[] KEY_STATE_PRESSED_HIGHLIGHT_OFF = {
871        android.R.attr.state_pressed,
872        android.R.attr.state_checkable
873    };
874
875    private final static int[] KEY_STATE_NORMAL = {
876    };
877
878    private final static int[] KEY_STATE_PRESSED = {
879        android.R.attr.state_pressed
880    };
881
882    private final static int[] KEY_STATE_EMPTY = {
883        android.R.attr.state_empty
884    };
885
886    // functional normal state (with properties)
887    private static final int[] KEY_STATE_FUNCTIONAL_NORMAL = {
888            android.R.attr.state_single
889    };
890
891    // functional pressed state (with properties)
892    private static final int[] KEY_STATE_FUNCTIONAL_PRESSED = {
893            android.R.attr.state_single,
894            android.R.attr.state_pressed
895    };
896
897    // action normal state (with properties)
898    private static final int[] KEY_STATE_ACTIVE_NORMAL = {
899            android.R.attr.state_active
900    };
901
902    // action pressed state (with properties)
903    private static final int[] KEY_STATE_ACTIVE_PRESSED = {
904            android.R.attr.state_active,
905            android.R.attr.state_pressed
906    };
907
908    /**
909     * Returns the drawable state for the key, based on the current state and type of the key.
910     * @return the drawable state of the key.
911     * @see android.graphics.drawable.StateListDrawable#setState(int[])
912     */
913    public final int[] getCurrentDrawableState() {
914        switch (mBackgroundType) {
915        case BACKGROUND_TYPE_FUNCTIONAL:
916            return mPressed ? KEY_STATE_FUNCTIONAL_PRESSED : KEY_STATE_FUNCTIONAL_NORMAL;
917        case BACKGROUND_TYPE_ACTION:
918            return mPressed ? KEY_STATE_ACTIVE_PRESSED : KEY_STATE_ACTIVE_NORMAL;
919        case BACKGROUND_TYPE_STICKY_OFF:
920            return mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_OFF : KEY_STATE_NORMAL_HIGHLIGHT_OFF;
921        case BACKGROUND_TYPE_STICKY_ON:
922            return mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_ON : KEY_STATE_NORMAL_HIGHLIGHT_ON;
923        case BACKGROUND_TYPE_EMPTY:
924            return mPressed ? KEY_STATE_PRESSED : KEY_STATE_EMPTY;
925        default: /* BACKGROUND_TYPE_NORMAL */
926            return mPressed ? KEY_STATE_PRESSED : KEY_STATE_NORMAL;
927        }
928    }
929
930    public static class Spacer extends Key {
931        public Spacer(final Resources res, final KeyboardParams params, final KeyboardRow row,
932                final XmlPullParser parser) throws XmlPullParserException {
933            super(res, params, row, parser);
934        }
935
936        /**
937         * This constructor is being used only for divider in more keys keyboard.
938         */
939        protected Spacer(final KeyboardParams params, final int x, final int y, final int width,
940                final int height) {
941            super(params, null, null, ICON_UNDEFINED, CODE_UNSPECIFIED,
942                    null, x, y, width, height, 0, BACKGROUND_TYPE_EMPTY);
943        }
944    }
945}
946