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