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