Key.java revision 9237a72634be821c22911633ef0848130e162d58
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.keyboard;
18
19import android.content.res.Resources;
20import android.content.res.TypedArray;
21import android.content.res.XmlResourceParser;
22import android.graphics.Typeface;
23import android.graphics.drawable.Drawable;
24import android.text.TextUtils;
25import android.util.Xml;
26
27import com.android.inputmethod.keyboard.internal.KeyStyles;
28import com.android.inputmethod.keyboard.internal.KeyStyles.KeyStyle;
29import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
30import com.android.inputmethod.keyboard.internal.KeyboardBuilder.ParseException;
31import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
32import com.android.inputmethod.keyboard.internal.KeyboardParams;
33import com.android.inputmethod.keyboard.internal.PopupCharactersParser;
34import com.android.inputmethod.keyboard.internal.Row;
35import com.android.inputmethod.latin.R;
36
37import java.util.HashMap;
38import java.util.Map;
39
40/**
41 * Class for describing the position and characteristics of a single key in the keyboard.
42 */
43public class Key {
44    /**
45     * The key code (unicode or custom code) that this key generates.
46     */
47    public final int mCode;
48
49    /** Label to display */
50    public final CharSequence mLabel;
51    /** Hint label to display on the key in conjunction with the label */
52    public final CharSequence mHintLabel;
53    /** Option of the label */
54    private final int mLabelOption;
55    private static final int LABEL_OPTION_ALIGN_LEFT = 0x01;
56    private static final int LABEL_OPTION_ALIGN_RIGHT = 0x02;
57    private static final int LABEL_OPTION_ALIGN_LEFT_OF_CENTER = 0x08;
58    private static final int LABEL_OPTION_LARGE_LETTER = 0x10;
59    private static final int LABEL_OPTION_FONT_NORMAL = 0x20;
60    private static final int LABEL_OPTION_FONT_MONO_SPACE = 0x40;
61    private static final int LABEL_OPTION_FOLLOW_KEY_LETTER_RATIO = 0x80;
62    private static final int LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO = 0x100;
63    private static final int LABEL_OPTION_HAS_POPUP_HINT = 0x200;
64    private static final int LABEL_OPTION_HAS_UPPERCASE_LETTER = 0x400;
65    private static final int LABEL_OPTION_HAS_HINT_LABEL = 0x800;
66    private static final int LABEL_OPTION_WITH_ICON_LEFT = 0x1000;
67    private static final int LABEL_OPTION_WITH_ICON_RIGHT = 0x2000;
68
69    /** Icon to display instead of a label. Icon takes precedence over a label */
70    private Drawable mIcon;
71    /** Preview version of the icon, for the preview popup */
72    private Drawable mPreviewIcon;
73
74    /** Width of the key, not including the gap */
75    public final int mWidth;
76    /** Height of the key, not including the gap */
77    public final int mHeight;
78    /** The horizontal gap around this key */
79    public final int mHorizontalGap;
80    /** The vertical gap below this key */
81    public final int mVerticalGap;
82    /** The visual insets */
83    public final int mVisualInsetsLeft;
84    public final int mVisualInsetsRight;
85    /** Whether this key is sticky, i.e., a toggle key */
86    public final boolean mSticky;
87    /** X coordinate of the key in the keyboard layout */
88    public final int mX;
89    /** Y coordinate of the key in the keyboard layout */
90    public final int mY;
91    /** Text to output when pressed. This can be multiple characters, like ".com" */
92    public final CharSequence mOutputText;
93    /** Popup characters */
94    public final CharSequence[] mPopupCharacters;
95    /** Popup keyboard maximum column number */
96    public final int mMaxMiniKeyboardColumn;
97
98    /**
99     * Flags that specify the anchoring to edges of the keyboard for detecting touch events
100     * that are just out of the boundary of the key. This is a bit mask of
101     * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT},
102     * {@link Keyboard#EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM}.
103     */
104    private int mEdgeFlags;
105    /** Whether this is a functional key which has different key top than normal key */
106    public final boolean mFunctional;
107    /** Whether this key repeats itself when held down */
108    public final boolean mRepeatable;
109
110    /** The current pressed state of this key */
111    private boolean mPressed;
112    /** If this is a sticky key, is its highlight on? */
113    private boolean mHighlightOn;
114    /** Key is enabled and responds on press */
115    private boolean mEnabled = true;
116    /** Whether this key needs to show the "..." popup hint for special purposes */
117    private boolean mNeedsSpecialPopupHint;
118
119    // keyWidth constants
120    private static final int KEYWIDTH_FILL_RIGHT = 0;
121    private static final int KEYWIDTH_FILL_BOTH = -1;
122
123    private final static int[] KEY_STATE_NORMAL_ON = {
124        android.R.attr.state_checkable,
125        android.R.attr.state_checked
126    };
127
128    private final static int[] KEY_STATE_PRESSED_ON = {
129        android.R.attr.state_pressed,
130        android.R.attr.state_checkable,
131        android.R.attr.state_checked
132    };
133
134    private final static int[] KEY_STATE_NORMAL_OFF = {
135        android.R.attr.state_checkable
136    };
137
138    private final static int[] KEY_STATE_PRESSED_OFF = {
139        android.R.attr.state_pressed,
140        android.R.attr.state_checkable
141    };
142
143    private final static int[] KEY_STATE_NORMAL = {
144    };
145
146    private final static int[] KEY_STATE_PRESSED = {
147        android.R.attr.state_pressed
148    };
149
150    // functional normal state (with properties)
151    private static final int[] KEY_STATE_FUNCTIONAL_NORMAL = {
152            android.R.attr.state_single
153    };
154
155    // functional pressed state (with properties)
156    private static final int[] KEY_STATE_FUNCTIONAL_PRESSED = {
157            android.R.attr.state_single,
158            android.R.attr.state_pressed
159    };
160
161    // RTL parenthesis character swapping map.
162    private static final Map<Integer, Integer> sRtlParenthesisMap = new HashMap<Integer, Integer>();
163
164    static {
165        // The all letters need to be mirrored are found at
166        // http://www.unicode.org/Public/6.0.0/ucd/extracted/DerivedBinaryProperties.txt
167        addRtlParenthesisPair('(', ')');
168        addRtlParenthesisPair('[', ']');
169        addRtlParenthesisPair('{', '}');
170        addRtlParenthesisPair('<', '>');
171        // \u00ab: LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
172        // \u00bb: RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
173        addRtlParenthesisPair('\u00ab', '\u00bb');
174        // \u2039: SINGLE LEFT-POINTING ANGLE QUOTATION MARK
175        // \u203a: SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
176        addRtlParenthesisPair('\u2039', '\u203a');
177        // \u2264: LESS-THAN OR EQUAL TO
178        // \u2265: GREATER-THAN OR EQUAL TO
179        addRtlParenthesisPair('\u2264', '\u2265');
180    }
181
182    private static void addRtlParenthesisPair(int left, int right) {
183        sRtlParenthesisMap.put(left, right);
184        sRtlParenthesisMap.put(right, left);
185    }
186
187    public static int getRtlParenthesisCode(int code, boolean isRtl) {
188        if (isRtl && sRtlParenthesisMap.containsKey(code)) {
189            return sRtlParenthesisMap.get(code);
190        } else {
191            return code;
192        }
193    }
194
195    private static int getCode(Resources res, KeyboardParams params, String popupSpec) {
196        return getRtlParenthesisCode(
197                PopupCharactersParser.getCode(res, popupSpec), params.mIsRtlKeyboard);
198    }
199
200    private static Drawable getIcon(KeyboardParams params, String popupSpec) {
201        return params.mIconsSet.getIcon(PopupCharactersParser.getIconId(popupSpec));
202    }
203
204    /**
205     * This constructor is being used only for key in popup mini keyboard.
206     */
207    public Key(Resources res, KeyboardParams params, String popupSpec,
208            int x, int y, int width, int height, int edgeFlags) {
209        this(params, PopupCharactersParser.getLabel(popupSpec), null, getIcon(params, popupSpec),
210                getCode(res, params, popupSpec), PopupCharactersParser.getOutputText(popupSpec),
211                x, y, width, height, edgeFlags);
212    }
213
214    /**
215     * This constructor is being used only for key in popup suggestions pane.
216     */
217    public Key(KeyboardParams params, CharSequence label, CharSequence hintLabel, Drawable icon,
218            int code, CharSequence outputText, int x, int y, int width, int height, int edgeFlags) {
219        mHeight = height - params.mVerticalGap;
220        mHorizontalGap = params.mHorizontalGap;
221        mVerticalGap = params.mVerticalGap;
222        mVisualInsetsLeft = mVisualInsetsRight = 0;
223        mWidth = width - mHorizontalGap;
224        mEdgeFlags = edgeFlags;
225        mHintLabel = hintLabel;
226        mLabelOption = 0;
227        mFunctional = false;
228        mSticky = false;
229        mRepeatable = false;
230        mPopupCharacters = null;
231        mMaxMiniKeyboardColumn = 0;
232        mLabel = label;
233        mOutputText = outputText;
234        mCode = code;
235        mIcon = icon;
236        // Horizontal gap is divided equally to both sides of the key.
237        mX = x + mHorizontalGap / 2;
238        mY = y;
239    }
240
241    /**
242     * Create a key with the given top-left coordinate and extract its attributes from the XML
243     * parser.
244     * @param res resources associated with the caller's context
245     * @param params the keyboard building parameters.
246     * @param row the row that this key belongs to.
247     * @param x the x coordinate of the top-left
248     * @param y the y coordinate of the top-left
249     * @param parser the XML parser containing the attributes for this key
250     * @param keyStyles active key styles set
251     */
252    public Key(Resources res, KeyboardParams params, Row row, int x, int y,
253            XmlResourceParser parser, KeyStyles keyStyles) {
254
255        final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
256                R.styleable.Keyboard);
257        int keyWidth;
258        try {
259            mHeight = KeyboardBuilder.getDimensionOrFraction(keyboardAttr,
260                    R.styleable.Keyboard_rowHeight,
261                    params.mHeight, row.mRowHeight) - params.mVerticalGap;
262            mHorizontalGap = KeyboardBuilder.getDimensionOrFraction(keyboardAttr,
263                    R.styleable.Keyboard_horizontalGap,
264                    params.mWidth, params.mHorizontalGap);
265            mVerticalGap = params.mVerticalGap;
266            keyWidth = KeyboardBuilder.getDimensionOrFraction(keyboardAttr,
267                    R.styleable.Keyboard_keyWidth,
268                    params.mWidth, row.mDefaultKeyWidth);
269        } finally {
270            keyboardAttr.recycle();
271        }
272
273        final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
274                R.styleable.Keyboard_Key);
275        try {
276            final KeyStyle style;
277            if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyStyle)) {
278                String styleName = keyAttr.getString(R.styleable.Keyboard_Key_keyStyle);
279                style = keyStyles.getKeyStyle(styleName);
280                if (style == null)
281                    throw new ParseException("Unknown key style: " + styleName, parser);
282            } else {
283                style = keyStyles.getEmptyKeyStyle();
284            }
285
286            final int keyboardWidth = params.mOccupiedWidth;
287            int keyXPos = KeyboardBuilder.getDimensionOrFraction(keyAttr,
288                    R.styleable.Keyboard_Key_keyXPos, keyboardWidth, x);
289            if (keyXPos < 0) {
290                // If keyXPos is negative, the actual x-coordinate will be k + keyXPos.
291                keyXPos += keyboardWidth;
292                if (keyXPos < x) {
293                    // keyXPos shouldn't be less than x because drawable area for this key starts
294                    // at x. Or, this key will overlaps the adjacent key on its left hand side.
295                    keyXPos = x;
296                }
297            }
298            if (keyWidth == KEYWIDTH_FILL_RIGHT) {
299                // If keyWidth is zero, the actual key width will be determined to fill out the
300                // area up to the right edge of the keyboard.
301                keyWidth = keyboardWidth - keyXPos;
302            } else if (keyWidth <= KEYWIDTH_FILL_BOTH) {
303                // If keyWidth is negative, the actual key width will be determined to fill out the
304                // area between the nearest key on the left hand side and the right edge of the
305                // keyboard.
306                keyXPos = x;
307                keyWidth = keyboardWidth - keyXPos;
308            }
309
310            // Horizontal gap is divided equally to both sides of the key.
311            mX = keyXPos + mHorizontalGap / 2;
312            mY = y;
313            mWidth = keyWidth - mHorizontalGap;
314
315            final CharSequence[] popupCharacters = style.getTextArray(
316                    keyAttr, R.styleable.Keyboard_Key_popupCharacters);
317            // In Arabic symbol layouts, we'd like to keep digits in popup characters regardless of
318            // config_digit_popup_characters_enabled.
319            if (params.mId.isAlphabetKeyboard() && !res.getBoolean(
320                    R.bool.config_digit_popup_characters_enabled)) {
321                mPopupCharacters = PopupCharactersParser.filterOut(
322                        res, popupCharacters, PopupCharactersParser.DIGIT_FILTER);
323            } else {
324                mPopupCharacters = popupCharacters;
325            }
326            mMaxMiniKeyboardColumn = style.getInt(keyboardAttr,
327                    R.styleable.Keyboard_Key_maxMiniKeyboardColumn,
328                    params.mMaxMiniKeyboardColumn);
329
330            mRepeatable = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isRepeatable, false);
331            mFunctional = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isFunctional, false);
332            mSticky = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isSticky, false);
333            mEnabled = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_enabled, true);
334            mEdgeFlags = 0;
335
336            final KeyboardIconsSet iconsSet = params.mIconsSet;
337            mVisualInsetsLeft = KeyboardBuilder.getDimensionOrFraction(keyAttr,
338                    R.styleable.Keyboard_Key_visualInsetsLeft, keyboardWidth, 0);
339            mVisualInsetsRight = KeyboardBuilder.getDimensionOrFraction(keyAttr,
340                    R.styleable.Keyboard_Key_visualInsetsRight, keyboardWidth, 0);
341            mPreviewIcon = iconsSet.getIcon(style.getInt(
342                    keyAttr, R.styleable.Keyboard_Key_keyIconPreview,
343                    KeyboardIconsSet.ICON_UNDEFINED));
344            mIcon = iconsSet.getIcon(style.getInt(
345                    keyAttr, R.styleable.Keyboard_Key_keyIcon,
346                    KeyboardIconsSet.ICON_UNDEFINED));
347            final int shiftedIconId = style.getInt(keyAttr, R.styleable.Keyboard_Key_keyIconShifted,
348                    KeyboardIconsSet.ICON_UNDEFINED);
349            if (shiftedIconId != KeyboardIconsSet.ICON_UNDEFINED) {
350                final Drawable shiftedIcon = iconsSet.getIcon(shiftedIconId);
351                params.addShiftedIcon(this, shiftedIcon);
352            }
353            mHintLabel = style.getText(keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
354
355            mLabel = style.getText(keyAttr, R.styleable.Keyboard_Key_keyLabel);
356            mLabelOption = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelOption, 0);
357            mOutputText = style.getText(keyAttr, R.styleable.Keyboard_Key_keyOutputText);
358            // Choose the first letter of the label as primary code if not
359            // specified.
360            final int code = style.getInt(keyAttr, R.styleable.Keyboard_Key_code,
361                    Keyboard.CODE_UNSPECIFIED);
362            if (code == Keyboard.CODE_UNSPECIFIED && !TextUtils.isEmpty(mLabel)) {
363                final int firstChar = mLabel.charAt(0);
364                mCode = getRtlParenthesisCode(firstChar, params.mIsRtlKeyboard);
365            } else if (code != Keyboard.CODE_UNSPECIFIED) {
366                mCode = code;
367            } else {
368                mCode = Keyboard.CODE_DUMMY;
369            }
370        } finally {
371            keyAttr.recycle();
372        }
373    }
374
375    public void addEdgeFlags(int flags) {
376        mEdgeFlags |= flags;
377    }
378
379    public Typeface selectTypeface(Typeface defaultTypeface) {
380        // TODO: Handle "bold" here too?
381        if ((mLabelOption & LABEL_OPTION_FONT_NORMAL) != 0) {
382            return Typeface.DEFAULT;
383        } else if ((mLabelOption & LABEL_OPTION_FONT_MONO_SPACE) != 0) {
384            return Typeface.MONOSPACE;
385        } else {
386            return defaultTypeface;
387        }
388    }
389
390    public int selectTextSize(int letter, int largeLetter, int label, int hintLabel) {
391        if (mLabel.length() > 1
392                && (mLabelOption & (LABEL_OPTION_FOLLOW_KEY_LETTER_RATIO
393                        | LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO)) == 0) {
394            return label;
395        } else if ((mLabelOption & LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO) != 0) {
396            return hintLabel;
397        } else if ((mLabelOption & LABEL_OPTION_LARGE_LETTER) != 0) {
398            return largeLetter;
399        } else {
400            return letter;
401        }
402    }
403
404    public boolean isAlignLeft() {
405        return (mLabelOption & LABEL_OPTION_ALIGN_LEFT) != 0;
406    }
407
408    public boolean isAlignRight() {
409        return (mLabelOption & LABEL_OPTION_ALIGN_RIGHT) != 0;
410    }
411
412    public boolean isAlignLeftOfCenter() {
413        return (mLabelOption & LABEL_OPTION_ALIGN_LEFT_OF_CENTER) != 0;
414    }
415
416    public boolean hasPopupHint() {
417        return (mLabelOption & LABEL_OPTION_HAS_POPUP_HINT) != 0;
418    }
419
420    public void setNeedsSpecialPopupHint(boolean needsSpecialPopupHint) {
421        mNeedsSpecialPopupHint = needsSpecialPopupHint;
422    }
423
424    public boolean needsSpecialPopupHint() {
425        return mNeedsSpecialPopupHint;
426    }
427
428    public boolean hasUppercaseLetter() {
429        return (mLabelOption & LABEL_OPTION_HAS_UPPERCASE_LETTER) != 0;
430    }
431
432    public boolean hasHintLabel() {
433        return (mLabelOption & LABEL_OPTION_HAS_HINT_LABEL) != 0;
434    }
435
436    public boolean hasLabelWithIconLeft() {
437        return (mLabelOption & LABEL_OPTION_WITH_ICON_LEFT) != 0;
438    }
439
440    public boolean hasLabelWithIconRight() {
441        return (mLabelOption & LABEL_OPTION_WITH_ICON_RIGHT) != 0;
442    }
443
444    public Drawable getIcon() {
445        return mIcon;
446    }
447
448    public Drawable getPreviewIcon() {
449        return mPreviewIcon;
450    }
451
452    public void setIcon(Drawable icon) {
453        mIcon = icon;
454    }
455
456    public void setPreviewIcon(Drawable icon) {
457        mPreviewIcon = icon;
458    }
459
460    /**
461     * Informs the key that it has been pressed, in case it needs to change its appearance or
462     * state.
463     * @see #onReleased()
464     */
465    public void onPressed() {
466        mPressed = true;
467    }
468
469    /**
470     * Informs the key that it has been released, in case it needs to change its appearance or
471     * state.
472     * @see #onPressed()
473     */
474    public void onReleased() {
475        mPressed = false;
476    }
477
478    public void setHighlightOn(boolean highlightOn) {
479        mHighlightOn = highlightOn;
480    }
481
482    public boolean isEnabled() {
483        return mEnabled;
484    }
485
486    public void setEnabled(boolean enabled) {
487        mEnabled = enabled;
488    }
489
490    /**
491     * Detects if a point falls on this key.
492     * @param x the x-coordinate of the point
493     * @param y the y-coordinate of the point
494     * @return whether or not the point falls on the key. If the key is attached to an edge, it will
495     * assume that all points between the key and the edge are considered to be on the key.
496     */
497    public boolean isOnKey(int x, int y) {
498        final int left = mX - mHorizontalGap / 2;
499        final int right = left + mWidth + mHorizontalGap;
500        final int top = mY;
501        final int bottom = top + mHeight + mVerticalGap;
502        final int flags = mEdgeFlags;
503        if (flags == 0) {
504            return x >= left && x <= right && y >= top && y <= bottom;
505        }
506        final boolean leftEdge = (flags & Keyboard.EDGE_LEFT) != 0;
507        final boolean rightEdge = (flags & Keyboard.EDGE_RIGHT) != 0;
508        final boolean topEdge = (flags & Keyboard.EDGE_TOP) != 0;
509        final boolean bottomEdge = (flags & Keyboard.EDGE_BOTTOM) != 0;
510        // In order to mitigate rounding errors, we use (left <= x <= right) here.
511        return (x >= left || leftEdge) && (x <= right || rightEdge)
512                && (y >= top || topEdge) && (y <= bottom || bottomEdge);
513    }
514
515    /**
516     * Returns the square of the distance to the nearest edge of the key and the given point.
517     * @param x the x-coordinate of the point
518     * @param y the y-coordinate of the point
519     * @return the square of the distance of the point from the nearest edge of the key
520     */
521    public int squaredDistanceToEdge(int x, int y) {
522        final int left = mX;
523        final int right = left + mWidth;
524        final int top = mY;
525        final int bottom = top + mHeight;
526        final int edgeX = x < left ? left : (x > right ? right : x);
527        final int edgeY = y < top ? top : (y > bottom ? bottom : y);
528        final int dx = x - edgeX;
529        final int dy = y - edgeY;
530        return dx * dx + dy * dy;
531    }
532
533    /**
534     * Returns the drawable state for the key, based on the current state and type of the key.
535     * @return the drawable state of the key.
536     * @see android.graphics.drawable.StateListDrawable#setState(int[])
537     */
538    public int[] getCurrentDrawableState() {
539        final boolean pressed = mPressed;
540        if (!mSticky && mFunctional) {
541            if (pressed) {
542                return KEY_STATE_FUNCTIONAL_PRESSED;
543            } else {
544                return KEY_STATE_FUNCTIONAL_NORMAL;
545            }
546        }
547
548        int[] states = KEY_STATE_NORMAL;
549
550        if (mHighlightOn) {
551            if (pressed) {
552                states = KEY_STATE_PRESSED_ON;
553            } else {
554                states = KEY_STATE_NORMAL_ON;
555            }
556        } else {
557            if (mSticky) {
558                if (pressed) {
559                    states = KEY_STATE_PRESSED_OFF;
560                } else {
561                    states = KEY_STATE_NORMAL_OFF;
562                }
563            } else {
564                if (pressed) {
565                    states = KEY_STATE_PRESSED;
566                }
567            }
568        }
569        return states;
570    }
571}
572