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