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