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