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    public final boolean hasCustomActionLabel() {
706        return (mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0;
707    }
708
709    private final boolean isShiftedLetterActivated() {
710        return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0
711                && !TextUtils.isEmpty(mHintLabel);
712    }
713
714    public final int getMoreKeysColumnNumber() {
715        return mMoreKeysColumnAndFlags & MORE_KEYS_COLUMN_NUMBER_MASK;
716    }
717
718    public final boolean isMoreKeysFixedColumn() {
719        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_COLUMN) != 0;
720    }
721
722    public final boolean isMoreKeysFixedOrder() {
723        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_ORDER) != 0;
724    }
725
726    public final boolean hasLabelsInMoreKeys() {
727        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_HAS_LABELS) != 0;
728    }
729
730    public final int getMoreKeyLabelFlags() {
731        final int labelSizeFlag = hasLabelsInMoreKeys()
732                ? LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
733                : LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO;
734        return labelSizeFlag | LABEL_FLAGS_AUTO_X_SCALE;
735    }
736
737    public final boolean needsDividersInMoreKeys() {
738        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NEEDS_DIVIDERS) != 0;
739    }
740
741    public final boolean hasNoPanelAutoMoreKey() {
742        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY) != 0;
743    }
744
745    public final String getOutputText() {
746        final OptionalAttributes attrs = mOptionalAttributes;
747        return (attrs != null) ? attrs.mOutputText : null;
748    }
749
750    public final int getAltCode() {
751        final OptionalAttributes attrs = mOptionalAttributes;
752        return (attrs != null) ? attrs.mAltCode : CODE_UNSPECIFIED;
753    }
754
755    public int getIconId() {
756        return mIconId;
757    }
758
759    public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) {
760        final OptionalAttributes attrs = mOptionalAttributes;
761        final int disabledIconId = (attrs != null) ? attrs.mDisabledIconId : ICON_UNDEFINED;
762        final int iconId = mEnabled ? getIconId() : disabledIconId;
763        final Drawable icon = iconSet.getIconDrawable(iconId);
764        if (icon != null) {
765            icon.setAlpha(alpha);
766        }
767        return icon;
768    }
769
770    public Drawable getPreviewIcon(final KeyboardIconsSet iconSet) {
771        return iconSet.getIconDrawable(getIconId());
772    }
773
774    public int getWidth() {
775        return mWidth;
776    }
777
778    public int getHeight() {
779        return mHeight;
780    }
781
782    public int getX() {
783        return mX;
784    }
785
786    public int getY() {
787        return mY;
788    }
789
790    public final int getDrawX() {
791        final int x = getX();
792        final OptionalAttributes attrs = mOptionalAttributes;
793        return (attrs == null) ? x : x + attrs.mVisualInsetsLeft;
794    }
795
796    public final int getDrawWidth() {
797        final OptionalAttributes attrs = mOptionalAttributes;
798        return (attrs == null) ? mWidth
799                : mWidth - attrs.mVisualInsetsLeft - attrs.mVisualInsetsRight;
800    }
801
802    /**
803     * Informs the key that it has been pressed, in case it needs to change its appearance or
804     * state.
805     * @see #onReleased()
806     */
807    public void onPressed() {
808        mPressed = true;
809    }
810
811    /**
812     * Informs the key that it has been released, in case it needs to change its appearance or
813     * state.
814     * @see #onPressed()
815     */
816    public void onReleased() {
817        mPressed = false;
818    }
819
820    public final boolean isEnabled() {
821        return mEnabled;
822    }
823
824    public void setEnabled(final boolean enabled) {
825        mEnabled = enabled;
826    }
827
828    public Rect getHitBox() {
829        return mHitBox;
830    }
831
832    /**
833     * Detects if a point falls on this key.
834     * @param x the x-coordinate of the point
835     * @param y the y-coordinate of the point
836     * @return whether or not the point falls on the key. If the key is attached to an edge, it
837     * will assume that all points between the key and the edge are considered to be on the key.
838     * @see #markAsLeftEdge(KeyboardParams) etc.
839     */
840    public boolean isOnKey(final int x, final int y) {
841        return mHitBox.contains(x, y);
842    }
843
844    /**
845     * Returns the square of the distance to the nearest edge of the key and the given point.
846     * @param x the x-coordinate of the point
847     * @param y the y-coordinate of the point
848     * @return the square of the distance of the point from the nearest edge of the key
849     */
850    public int squaredDistanceToEdge(final int x, final int y) {
851        final int left = getX();
852        final int right = left + mWidth;
853        final int top = getY();
854        final int bottom = top + mHeight;
855        final int edgeX = x < left ? left : (x > right ? right : x);
856        final int edgeY = y < top ? top : (y > bottom ? bottom : y);
857        final int dx = x - edgeX;
858        final int dy = y - edgeY;
859        return dx * dx + dy * dy;
860    }
861
862    static class KeyBackgroundState {
863        private final int[] mReleasedState;
864        private final int[] mPressedState;
865
866        private KeyBackgroundState(final int ... attrs) {
867            mReleasedState = attrs;
868            mPressedState = Arrays.copyOf(attrs, attrs.length + 1);
869            mPressedState[attrs.length] = android.R.attr.state_pressed;
870        }
871
872        public int[] getState(final boolean pressed) {
873            return pressed ? mPressedState : mReleasedState;
874        }
875
876        public static final KeyBackgroundState[] STATES = {
877            // 0: BACKGROUND_TYPE_EMPTY
878            new KeyBackgroundState(android.R.attr.state_empty),
879            // 1: BACKGROUND_TYPE_NORMAL
880            new KeyBackgroundState(),
881            // 2: BACKGROUND_TYPE_FUNCTIONAL
882            new KeyBackgroundState(),
883            // 3: BACKGROUND_TYPE_STICKY_OFF
884            new KeyBackgroundState(android.R.attr.state_checkable),
885            // 4: BACKGROUND_TYPE_STICKY_ON
886            new KeyBackgroundState(android.R.attr.state_checkable, android.R.attr.state_checked),
887            // 5: BACKGROUND_TYPE_ACTION
888            new KeyBackgroundState(android.R.attr.state_active),
889            // 6: BACKGROUND_TYPE_SPACEBAR
890            new KeyBackgroundState(),
891        };
892    }
893
894    /**
895     * Returns the background drawable for the key, based on the current state and type of the key.
896     * @return the background drawable of the key.
897     * @see android.graphics.drawable.StateListDrawable#setState(int[])
898     */
899    public final Drawable selectBackgroundDrawable(final Drawable keyBackground,
900            final Drawable functionalKeyBackground, final Drawable spacebarBackground) {
901        final Drawable background;
902        if (mBackgroundType == BACKGROUND_TYPE_FUNCTIONAL) {
903            background = functionalKeyBackground;
904        } else if (mBackgroundType == BACKGROUND_TYPE_SPACEBAR) {
905            background = spacebarBackground;
906        } else {
907            background = keyBackground;
908        }
909        final int[] state = KeyBackgroundState.STATES[mBackgroundType].getState(mPressed);
910        background.setState(state);
911        return background;
912    }
913
914    public static class Spacer extends Key {
915        public Spacer(final TypedArray keyAttr, final KeyStyle keyStyle,
916                final KeyboardParams params, final KeyboardRow row) {
917            super(null /* keySpec */, keyAttr, keyStyle, params, row);
918        }
919
920        /**
921         * This constructor is being used only for divider in more keys keyboard.
922         */
923        protected Spacer(final KeyboardParams params, final int x, final int y, final int width,
924                final int height) {
925            super(null /* label */, ICON_UNDEFINED, CODE_UNSPECIFIED, null /* outputText */,
926                    null /* hintLabel */, 0 /* labelFlags */, BACKGROUND_TYPE_EMPTY, x, y, width,
927                    height, params.mHorizontalGap, params.mVerticalGap);
928        }
929    }
930}
931