Key.java revision a83a1feb62c4b4ff1a7cf5b6f58ad115491de76f
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        if (TextUtils.isEmpty(keySpec)) {
320            throw new RuntimeException("Empty keySpec");
321        }
322
323        mIconId = 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 code = KeySpecParser.getCode(keySpec, params.mCodesSet);
330        if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) {
331            mLabel = params.mId.mCustomActionLabel;
332        } else if (code >= Character.MIN_SUPPLEMENTARY_CODE_POINT) {
333            // This is a workaround to have a key that has a supplementary code point in its label.
334            // Because we can put a string in resource neither as a XML entity of a supplementary
335            // code point nor as a surrogate pair.
336            mLabel = new StringBuilder().appendCodePoint(code).toString();
337        } else {
338            mLabel = StringUtils.toUpperCaseOfStringForLocale(
339                    KeySpecParser.getLabel(keySpec), needsToUpperCase, locale);
340        }
341        if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) {
342            mHintLabel = null;
343        } else {
344            mHintLabel = StringUtils.toUpperCaseOfStringForLocale(style.getString(keyAttr,
345                    R.styleable.Keyboard_Key_keyHintLabel), needsToUpperCase, locale);
346        }
347        String outputText = StringUtils.toUpperCaseOfStringForLocale(
348                KeySpecParser.getOutputText(keySpec), needsToUpperCase, locale);
349        // Choose the first letter of the label as primary code if not specified.
350        if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText)
351                && !TextUtils.isEmpty(mLabel)) {
352            if (StringUtils.codePointCount(mLabel) == 1) {
353                // Use the first letter of the hint label if shiftedLetterActivated flag is
354                // specified.
355                if (hasShiftedLetterHint() && isShiftedLetterActivated()) {
356                    mCode = mHintLabel.codePointAt(0);
357                } else {
358                    mCode = mLabel.codePointAt(0);
359                }
360            } else {
361                // In some locale and case, the character might be represented by multiple code
362                // points, such as upper case Eszett of German alphabet.
363                outputText = mLabel;
364                mCode = CODE_OUTPUT_TEXT;
365            }
366        } else if (code == CODE_UNSPECIFIED && outputText != null) {
367            if (StringUtils.codePointCount(outputText) == 1) {
368                mCode = outputText.codePointAt(0);
369                outputText = null;
370            } else {
371                mCode = CODE_OUTPUT_TEXT;
372            }
373        } else {
374            mCode = StringUtils.toUpperCaseOfCodeForLocale(code, needsToUpperCase, locale);
375        }
376        final int altCodeInAttr = KeySpecParser.parseCode(style.getString(keyAttr,
377                R.styleable.Keyboard_Key_altCode), params.mCodesSet, CODE_UNSPECIFIED);
378        final int altCode = StringUtils.toUpperCaseOfCodeForLocale(
379                altCodeInAttr, needsToUpperCase, locale);
380        mOptionalAttributes = OptionalAttributes.newInstance(outputText, altCode,
381                disabledIconId, previewIconId, visualInsetsLeft, visualInsetsRight);
382        mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
383        keyAttr.recycle();
384        mHashCode = computeHashCode(this);
385    }
386
387    /**
388     * Copy constructor.
389     *
390     * @param key the original key.
391     */
392    protected Key(final Key key) {
393        // Final attributes.
394        mCode = key.mCode;
395        mLabel = key.mLabel;
396        mHintLabel = key.mHintLabel;
397        mLabelFlags = key.mLabelFlags;
398        mIconId = key.mIconId;
399        mWidth = key.mWidth;
400        mHeight = key.mHeight;
401        mX = key.mX;
402        mY = key.mY;
403        mHitBox.set(key.mHitBox);
404        mMoreKeys = key.mMoreKeys;
405        mMoreKeysColumnAndFlags = key.mMoreKeysColumnAndFlags;
406        mBackgroundType = key.mBackgroundType;
407        mActionFlags = key.mActionFlags;
408        mKeyVisualAttributes = key.mKeyVisualAttributes;
409        mOptionalAttributes = key.mOptionalAttributes;
410        mHashCode = key.mHashCode;
411        // Key state.
412        mPressed = key.mPressed;
413        mEnabled = key.mEnabled;
414    }
415
416    private static boolean needsToUpperCase(final int labelFlags, final int keyboardElementId) {
417        if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false;
418        switch (keyboardElementId) {
419        case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
420        case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
421        case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
422        case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
423            return true;
424        default:
425            return false;
426        }
427    }
428
429    private static int computeHashCode(final Key key) {
430        return Arrays.hashCode(new Object[] {
431                key.mX,
432                key.mY,
433                key.mWidth,
434                key.mHeight,
435                key.mCode,
436                key.mLabel,
437                key.mHintLabel,
438                key.mIconId,
439                key.mBackgroundType,
440                Arrays.hashCode(key.mMoreKeys),
441                key.getOutputText(),
442                key.mActionFlags,
443                key.mLabelFlags,
444                // Key can be distinguishable without the following members.
445                // key.mOptionalAttributes.mAltCode,
446                // key.mOptionalAttributes.mDisabledIconId,
447                // key.mOptionalAttributes.mPreviewIconId,
448                // key.mHorizontalGap,
449                // key.mVerticalGap,
450                // key.mOptionalAttributes.mVisualInsetLeft,
451                // key.mOptionalAttributes.mVisualInsetRight,
452                // key.mMaxMoreKeysColumn,
453        });
454    }
455
456    private boolean equalsInternal(final Key o) {
457        if (this == o) return true;
458        return o.mX == mX
459                && o.mY == mY
460                && o.mWidth == mWidth
461                && o.mHeight == mHeight
462                && o.mCode == mCode
463                && TextUtils.equals(o.mLabel, mLabel)
464                && TextUtils.equals(o.mHintLabel, mHintLabel)
465                && o.mIconId == mIconId
466                && o.mBackgroundType == mBackgroundType
467                && Arrays.equals(o.mMoreKeys, mMoreKeys)
468                && TextUtils.equals(o.getOutputText(), getOutputText())
469                && o.mActionFlags == mActionFlags
470                && o.mLabelFlags == mLabelFlags;
471    }
472
473    @Override
474    public int compareTo(Key o) {
475        if (equalsInternal(o)) return 0;
476        if (mHashCode > o.mHashCode) return 1;
477        return -1;
478    }
479
480    @Override
481    public int hashCode() {
482        return mHashCode;
483    }
484
485    @Override
486    public boolean equals(final Object o) {
487        return o instanceof Key && equalsInternal((Key)o);
488    }
489
490    @Override
491    public String toString() {
492        final String label;
493        if (StringUtils.codePointCount(mLabel) == 1 && mLabel.codePointAt(0) == mCode) {
494            label = "";
495        } else {
496            label = "/" + mLabel;
497        }
498        return String.format(Locale.ROOT, "%s%s %d,%d %dx%d %s/%s/%s",
499                Constants.printableCode(mCode), label, mX, mY, mWidth, mHeight, mHintLabel,
500                KeyboardIconsSet.getIconName(mIconId), backgroundName(mBackgroundType));
501    }
502
503    private static String backgroundName(final int backgroundType) {
504        switch (backgroundType) {
505        case BACKGROUND_TYPE_EMPTY: return "empty";
506        case BACKGROUND_TYPE_NORMAL: return "normal";
507        case BACKGROUND_TYPE_FUNCTIONAL: return "functional";
508        case BACKGROUND_TYPE_ACTION: return "action";
509        case BACKGROUND_TYPE_STICKY_OFF: return "stickyOff";
510        case BACKGROUND_TYPE_STICKY_ON: return "stickyOn";
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 isShift() {
552        return mCode == CODE_SHIFT;
553    }
554
555    public final boolean isModifier() {
556        return mCode == CODE_SHIFT || mCode == CODE_SWITCH_ALPHA_SYMBOL;
557    }
558
559    public final boolean isRepeatable() {
560        return (mActionFlags & ACTION_FLAGS_IS_REPEATABLE) != 0;
561    }
562
563    public final boolean noKeyPreview() {
564        return (mActionFlags & ACTION_FLAGS_NO_KEY_PREVIEW) != 0;
565    }
566
567    public final boolean altCodeWhileTyping() {
568        return (mActionFlags & ACTION_FLAGS_ALT_CODE_WHILE_TYPING) != 0;
569    }
570
571    public final boolean isLongPressEnabled() {
572        // We need not start long press timer on the key which has activated shifted letter.
573        return (mActionFlags & ACTION_FLAGS_ENABLE_LONG_PRESS) != 0
574                && (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) == 0;
575    }
576
577    public KeyVisualAttributes getVisualAttributes() {
578        return mKeyVisualAttributes;
579    }
580
581    public final Typeface selectTypeface(final KeyDrawParams params) {
582        // TODO: Handle "bold" here too?
583        if ((mLabelFlags & LABEL_FLAGS_FONT_NORMAL) != 0) {
584            return Typeface.DEFAULT;
585        }
586        if ((mLabelFlags & LABEL_FLAGS_FONT_MONO_SPACE) != 0) {
587            return Typeface.MONOSPACE;
588        }
589        return params.mTypeface;
590    }
591
592    public final int selectTextSize(final KeyDrawParams params) {
593        switch (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK) {
594        case LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO:
595            return params.mLetterSize;
596        case LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO:
597            return params.mLargeLetterSize;
598        case LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO:
599            return params.mLabelSize;
600        case LABEL_FLAGS_FOLLOW_KEY_LARGE_LABEL_RATIO:
601            return params.mLargeLabelSize;
602        case LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO:
603            return params.mHintLabelSize;
604        default: // No follow key ratio flag specified.
605            return StringUtils.codePointCount(mLabel) == 1 ? params.mLetterSize : params.mLabelSize;
606        }
607    }
608
609    public final int selectTextColor(final KeyDrawParams params) {
610        if (isShiftedLetterActivated()) {
611            return params.mTextInactivatedColor;
612        }
613        if (params.mTextColorStateList == null) {
614            return DEFAULT_TEXT_COLOR;
615        }
616        final int[] state;
617        // TODO: Hack!!!!!!!! Consider having a new attribute for the functional text labels.
618        // Currently, we distinguish "input key" from "functional key" by checking the
619        // length of the label( > 1) and "functional" attributes (= true).
620        if (mLabel != null && mLabel.length() > 1) {
621            state = getCurrentDrawableState();
622        } else {
623            state = KEY_STATE_NORMAL;
624        }
625        return params.mTextColorStateList.getColorForState(state, DEFAULT_TEXT_COLOR);
626    }
627
628    public final int selectHintTextSize(final KeyDrawParams params) {
629        if (hasHintLabel()) {
630            return params.mHintLabelSize;
631        }
632        if (hasShiftedLetterHint()) {
633            return params.mShiftedLetterHintSize;
634        }
635        return params.mHintLetterSize;
636    }
637
638    public final int selectHintTextColor(final KeyDrawParams params) {
639        if (hasHintLabel()) {
640            return params.mHintLabelColor;
641        }
642        if (hasShiftedLetterHint()) {
643            return isShiftedLetterActivated() ? params.mShiftedLetterHintActivatedColor
644                    : params.mShiftedLetterHintInactivatedColor;
645        }
646        return params.mHintLetterColor;
647    }
648
649    public final int selectMoreKeyTextSize(final KeyDrawParams params) {
650        return hasLabelsInMoreKeys() ? params.mLabelSize : params.mLetterSize;
651    }
652
653    public final String getPreviewLabel() {
654        return isShiftedLetterActivated() ? mHintLabel : mLabel;
655    }
656
657    private boolean previewHasLetterSize() {
658        return (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO) != 0
659                || StringUtils.codePointCount(getPreviewLabel()) == 1;
660    }
661
662    public final int selectPreviewTextSize(final KeyDrawParams params) {
663        if (previewHasLetterSize()) {
664            return params.mPreviewTextSize;
665        }
666        return params.mLetterSize;
667    }
668
669    public Typeface selectPreviewTypeface(final KeyDrawParams params) {
670        if (previewHasLetterSize()) {
671            return selectTypeface(params);
672        }
673        return Typeface.DEFAULT_BOLD;
674    }
675
676    public final boolean isAlignLeft() {
677        return (mLabelFlags & LABEL_FLAGS_ALIGN_LEFT) != 0;
678    }
679
680    public final boolean isAlignRight() {
681        return (mLabelFlags & LABEL_FLAGS_ALIGN_RIGHT) != 0;
682    }
683
684    public final boolean isAlignLeftOfCenter() {
685        return (mLabelFlags & LABEL_FLAGS_ALIGN_LEFT_OF_CENTER) != 0;
686    }
687
688    public final boolean hasPopupHint() {
689        return (mLabelFlags & LABEL_FLAGS_HAS_POPUP_HINT) != 0;
690    }
691
692    public final boolean hasShiftedLetterHint() {
693        return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0
694                && !TextUtils.isEmpty(mHintLabel);
695    }
696
697    public final boolean hasHintLabel() {
698        return (mLabelFlags & LABEL_FLAGS_HAS_HINT_LABEL) != 0;
699    }
700
701    public final boolean hasLabelWithIconLeft() {
702        return (mLabelFlags & LABEL_FLAGS_WITH_ICON_LEFT) != 0;
703    }
704
705    public final boolean hasLabelWithIconRight() {
706        return (mLabelFlags & LABEL_FLAGS_WITH_ICON_RIGHT) != 0;
707    }
708
709    public final boolean needsAutoXScale() {
710        return (mLabelFlags & LABEL_FLAGS_AUTO_X_SCALE) != 0;
711    }
712
713    public final boolean needsAutoScale() {
714        return (mLabelFlags & LABEL_FLAGS_AUTO_SCALE) == LABEL_FLAGS_AUTO_SCALE;
715    }
716
717    private final boolean isShiftedLetterActivated() {
718        return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0
719                && !TextUtils.isEmpty(mHintLabel);
720    }
721
722    public final int getMoreKeysColumn() {
723        return mMoreKeysColumnAndFlags & MORE_KEYS_COLUMN_MASK;
724    }
725
726    public final boolean isFixedColumnOrderMoreKeys() {
727        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER) != 0;
728    }
729
730    public final boolean hasLabelsInMoreKeys() {
731        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_HAS_LABELS) != 0;
732    }
733
734    public final int getMoreKeyLabelFlags() {
735        return hasLabelsInMoreKeys()
736                ? LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
737                : LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO;
738    }
739
740    public final boolean needsDividersInMoreKeys() {
741        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NEEDS_DIVIDERS) != 0;
742    }
743
744    public final boolean hasNoPanelAutoMoreKey() {
745        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY) != 0;
746    }
747
748    public final String getOutputText() {
749        final OptionalAttributes attrs = mOptionalAttributes;
750        return (attrs != null) ? attrs.mOutputText : null;
751    }
752
753    public final int getAltCode() {
754        final OptionalAttributes attrs = mOptionalAttributes;
755        return (attrs != null) ? attrs.mAltCode : CODE_UNSPECIFIED;
756    }
757
758    public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) {
759        final OptionalAttributes attrs = mOptionalAttributes;
760        final int disabledIconId = (attrs != null) ? attrs.mDisabledIconId : ICON_UNDEFINED;
761        final int iconId = mEnabled ? mIconId : disabledIconId;
762        final Drawable icon = iconSet.getIconDrawable(iconId);
763        if (icon != null) {
764            icon.setAlpha(alpha);
765        }
766        return icon;
767    }
768
769    public Drawable getPreviewIcon(final KeyboardIconsSet iconSet) {
770        final OptionalAttributes attrs = mOptionalAttributes;
771        final int previewIconId = (attrs != null) ? attrs.mPreviewIconId : ICON_UNDEFINED;
772        return previewIconId != ICON_UNDEFINED
773                ? iconSet.getIconDrawable(previewIconId) : iconSet.getIconDrawable(mIconId);
774    }
775
776    public int getWidth() {
777        return mWidth;
778    }
779
780    public int getHeight() {
781        return mHeight;
782    }
783
784    public int getX() {
785        return mX;
786    }
787
788    public int getY() {
789        return mY;
790    }
791
792    public final int getDrawX() {
793        final int x = getX();
794        final OptionalAttributes attrs = mOptionalAttributes;
795        return (attrs == null) ? x : x + attrs.mVisualInsetsLeft;
796    }
797
798    public final int getDrawWidth() {
799        final OptionalAttributes attrs = mOptionalAttributes;
800        return (attrs == null) ? mWidth
801                : mWidth - attrs.mVisualInsetsLeft - attrs.mVisualInsetsRight;
802    }
803
804    /**
805     * Informs the key that it has been pressed, in case it needs to change its appearance or
806     * state.
807     * @see #onReleased()
808     */
809    public void onPressed() {
810        mPressed = true;
811    }
812
813    /**
814     * Informs the key that it has been released, in case it needs to change its appearance or
815     * state.
816     * @see #onPressed()
817     */
818    public void onReleased() {
819        mPressed = false;
820    }
821
822    public final boolean isEnabled() {
823        return mEnabled;
824    }
825
826    public void setEnabled(final boolean enabled) {
827        mEnabled = enabled;
828    }
829
830    public Rect getHitBox() {
831        return mHitBox;
832    }
833
834    /**
835     * Detects if a point falls on this key.
836     * @param x the x-coordinate of the point
837     * @param y the y-coordinate of the point
838     * @return whether or not the point falls on the key. If the key is attached to an edge, it
839     * will assume that all points between the key and the edge are considered to be on the key.
840     * @see #markAsLeftEdge(KeyboardParams) etc.
841     */
842    public boolean isOnKey(final int x, final int y) {
843        return mHitBox.contains(x, y);
844    }
845
846    /**
847     * Returns the square of the distance to the nearest edge of the key and the given point.
848     * @param x the x-coordinate of the point
849     * @param y the y-coordinate of the point
850     * @return the square of the distance of the point from the nearest edge of the key
851     */
852    public int squaredDistanceToEdge(final int x, final int y) {
853        final int left = getX();
854        final int right = left + mWidth;
855        final int top = getY();
856        final int bottom = top + mHeight;
857        final int edgeX = x < left ? left : (x > right ? right : x);
858        final int edgeY = y < top ? top : (y > bottom ? bottom : y);
859        final int dx = x - edgeX;
860        final int dy = y - edgeY;
861        return dx * dx + dy * dy;
862    }
863
864    private final static int[] KEY_STATE_NORMAL_HIGHLIGHT_ON = {
865        android.R.attr.state_checkable,
866        android.R.attr.state_checked
867    };
868
869    private final static int[] KEY_STATE_PRESSED_HIGHLIGHT_ON = {
870        android.R.attr.state_pressed,
871        android.R.attr.state_checkable,
872        android.R.attr.state_checked
873    };
874
875    private final static int[] KEY_STATE_NORMAL_HIGHLIGHT_OFF = {
876        android.R.attr.state_checkable
877    };
878
879    private final static int[] KEY_STATE_PRESSED_HIGHLIGHT_OFF = {
880        android.R.attr.state_pressed,
881        android.R.attr.state_checkable
882    };
883
884    private final static int[] KEY_STATE_NORMAL = {
885    };
886
887    private final static int[] KEY_STATE_PRESSED = {
888        android.R.attr.state_pressed
889    };
890
891    private final static int[] KEY_STATE_EMPTY = {
892        android.R.attr.state_empty
893    };
894
895    // functional normal state (with properties)
896    private static final int[] KEY_STATE_FUNCTIONAL_NORMAL = {
897            android.R.attr.state_single
898    };
899
900    // functional pressed state (with properties)
901    private static final int[] KEY_STATE_FUNCTIONAL_PRESSED = {
902            android.R.attr.state_single,
903            android.R.attr.state_pressed
904    };
905
906    // action normal state (with properties)
907    private static final int[] KEY_STATE_ACTIVE_NORMAL = {
908            android.R.attr.state_active
909    };
910
911    // action pressed state (with properties)
912    private static final int[] KEY_STATE_ACTIVE_PRESSED = {
913            android.R.attr.state_active,
914            android.R.attr.state_pressed
915    };
916
917    /**
918     * Returns the drawable state for the key, based on the current state and type of the key.
919     * @return the drawable state of the key.
920     * @see android.graphics.drawable.StateListDrawable#setState(int[])
921     */
922    public final int[] getCurrentDrawableState() {
923        switch (mBackgroundType) {
924        case BACKGROUND_TYPE_FUNCTIONAL:
925            return mPressed ? KEY_STATE_FUNCTIONAL_PRESSED : KEY_STATE_FUNCTIONAL_NORMAL;
926        case BACKGROUND_TYPE_ACTION:
927            return mPressed ? KEY_STATE_ACTIVE_PRESSED : KEY_STATE_ACTIVE_NORMAL;
928        case BACKGROUND_TYPE_STICKY_OFF:
929            return mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_OFF : KEY_STATE_NORMAL_HIGHLIGHT_OFF;
930        case BACKGROUND_TYPE_STICKY_ON:
931            return mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_ON : KEY_STATE_NORMAL_HIGHLIGHT_ON;
932        case BACKGROUND_TYPE_EMPTY:
933            return mPressed ? KEY_STATE_PRESSED : KEY_STATE_EMPTY;
934        default: /* BACKGROUND_TYPE_NORMAL */
935            return mPressed ? KEY_STATE_PRESSED : KEY_STATE_NORMAL;
936        }
937    }
938
939    public static class Spacer extends Key {
940        public Spacer(final Resources res, final KeyboardParams params, final KeyboardRow row,
941                final XmlPullParser parser) throws XmlPullParserException {
942            super(res, params, row, parser);
943        }
944
945        /**
946         * This constructor is being used only for divider in more keys keyboard.
947         */
948        protected Spacer(final KeyboardParams params, final int x, final int y, final int width,
949                final int height) {
950            super(params, null, null, ICON_UNDEFINED, CODE_UNSPECIFIED,
951                    null, x, y, width, height, 0, BACKGROUND_TYPE_EMPTY);
952        }
953    }
954}
955