Keyboard.java revision d773bf38a3c8f49ea56de67d3b828f8126f46ed2
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.Context;
20import android.content.res.Resources;
21import android.graphics.drawable.Drawable;
22import android.text.TextUtils;
23import android.util.Log;
24
25import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
26import com.android.inputmethod.keyboard.internal.KeyboardParser;
27import com.android.inputmethod.keyboard.internal.KeyboardShiftState;
28import com.android.inputmethod.latin.R;
29
30import org.xmlpull.v1.XmlPullParserException;
31
32import java.io.IOException;
33import java.util.ArrayList;
34import java.util.HashMap;
35import java.util.HashSet;
36import java.util.List;
37import java.util.Map;
38
39/**
40 * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
41 * consists of rows of keys.
42 * <p>The layout file for a keyboard contains XML that looks like the following snippet:</p>
43 * <pre>
44 * &lt;Keyboard
45 *         latin:keyWidth="%10p"
46 *         latin:keyHeight="50px"
47 *         latin:horizontalGap="2px"
48 *         latin:verticalGap="2px" &gt;
49 *     &lt;Row latin:keyWidth="32px" &gt;
50 *         &lt;Key latin:keyLabel="A" /&gt;
51 *         ...
52 *     &lt;/Row&gt;
53 *     ...
54 * &lt;/Keyboard&gt;
55 * </pre>
56 */
57public class Keyboard {
58    private static final String TAG = Keyboard.class.getSimpleName();
59
60    public static final int EDGE_LEFT = 0x01;
61    public static final int EDGE_RIGHT = 0x02;
62    public static final int EDGE_TOP = 0x04;
63    public static final int EDGE_BOTTOM = 0x08;
64
65    /** Some common keys code.  These should be aligned with values/keycodes.xml */
66    public static final int CODE_ENTER = '\n';
67    public static final int CODE_TAB = '\t';
68    public static final int CODE_SPACE = ' ';
69    public static final int CODE_PERIOD = '.';
70    public static final int CODE_DASH = '-';
71    public static final int CODE_SINGLE_QUOTE = '\'';
72    public static final int CODE_DOUBLE_QUOTE = '"';
73
74    /** Special keys code.  These should be aligned with values/keycodes.xml */
75    public static final int CODE_DUMMY = 0;
76    public static final int CODE_SHIFT = -1;
77    public static final int CODE_SWITCH_ALPHA_SYMBOL = -2;
78    public static final int CODE_CAPSLOCK = -3;
79    public static final int CODE_CANCEL = -4;
80    public static final int CODE_DELETE = -5;
81    public static final int CODE_SETTINGS = -6;
82    public static final int CODE_SETTINGS_LONGPRESS = -7;
83    public static final int CODE_SHORTCUT = -8;
84    // Code value representing the code is not specified.
85    public static final int CODE_UNSPECIFIED = -99;
86
87    /** Horizontal gap default for all rows */
88    private int mDefaultHorizontalGap;
89
90    /** Default key width */
91    private int mDefaultWidth;
92
93    /** Default key height */
94    private int mDefaultHeight;
95
96    /** Default gap between rows */
97    private int mDefaultVerticalGap;
98
99    /** Popup keyboard template */
100    private int mPopupKeyboardResId;
101
102    /** Maximum column for popup keyboard */
103    private int mMaxPopupColumn;
104
105    /** List of shift keys in this keyboard and its icons and state */
106    private final List<Key> mShiftKeys = new ArrayList<Key>();
107    private final HashMap<Key, Drawable> mShiftedIcons = new HashMap<Key, Drawable>();
108    private final HashMap<Key, Drawable> mNormalShiftIcons = new HashMap<Key, Drawable>();
109    private final HashSet<Key> mShiftLockEnabled = new HashSet<Key>();
110    private final KeyboardShiftState mShiftState = new KeyboardShiftState();
111
112    /** Total height of the keyboard, including the padding and keys */
113    private int mTotalHeight;
114
115    /**
116     * Total width (minimum width) of the keyboard, including left side gaps and keys, but not any
117     * gaps on the right side.
118     */
119    private int mMinWidth;
120
121    /** List of keys in this keyboard */
122    private final List<Key> mKeys = new ArrayList<Key>();
123
124    /** Width of the screen available to fit the keyboard */
125    private final int mDisplayWidth;
126
127    /** Height of the screen */
128    private final int mDisplayHeight;
129
130    /** Height of keyboard */
131    private int mKeyboardHeight;
132
133    private int mMostCommonKeyWidth = 0;
134
135    public final KeyboardId mId;
136
137    public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet();
138
139    // Variables for pre-computing nearest keys.
140
141    // TODO: Change GRID_WIDTH and GRID_HEIGHT to private.
142    public final int GRID_WIDTH;
143    public final int GRID_HEIGHT;
144    private final int GRID_SIZE;
145    private int mCellWidth;
146    private int mCellHeight;
147    private int[][] mGridNeighbors;
148    private int mProximityThreshold;
149    private static int[] EMPTY_INT_ARRAY = new int[0];
150    /** Number of key widths from current touch point to search for nearest keys. */
151    private static float SEARCH_DISTANCE = 1.2f;
152
153    private final ProximityInfo mProximityInfo;
154
155    /**
156     * Creates a keyboard from the given xml key layout file.
157     * @param context the application or service context
158     * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
159     * @param id keyboard identifier
160     * @param width keyboard width
161     */
162
163    public Keyboard(Context context, int xmlLayoutResId, KeyboardId id, int width) {
164        final Resources res = context.getResources();
165        GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
166        GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
167        GRID_SIZE = GRID_WIDTH * GRID_HEIGHT;
168
169        final int horizontalEdgesPadding = (int)res.getDimension(
170                R.dimen.keyboard_horizontal_edges_padding);
171        mDisplayWidth = width - horizontalEdgesPadding * 2;
172        // TODO: Adjust the height by referring to the height of area available for drawing as well.
173        mDisplayHeight = res.getDisplayMetrics().heightPixels;
174
175        mDefaultHorizontalGap = 0;
176        setKeyWidth(mDisplayWidth / 10);
177        mDefaultVerticalGap = 0;
178        mDefaultHeight = mDefaultWidth;
179        mId = id;
180        mProximityInfo = new ProximityInfo(GRID_WIDTH, GRID_HEIGHT);
181        loadKeyboard(context, xmlLayoutResId);
182    }
183
184    public int getProximityInfo() {
185        return mProximityInfo.getNativeProximityInfo(this);
186    }
187
188    public List<Key> getKeys() {
189        return mKeys;
190    }
191
192    public int getHorizontalGap() {
193        return mDefaultHorizontalGap;
194    }
195
196    public void setHorizontalGap(int gap) {
197        mDefaultHorizontalGap = gap;
198    }
199
200    public int getVerticalGap() {
201        return mDefaultVerticalGap;
202    }
203
204    public void setVerticalGap(int gap) {
205        mDefaultVerticalGap = gap;
206    }
207
208    public int getRowHeight() {
209        return mDefaultHeight;
210    }
211
212    public void setRowHeight(int height) {
213        mDefaultHeight = height;
214    }
215
216    public int getKeyWidth() {
217        return mDefaultWidth;
218    }
219
220    public void setKeyWidth(int width) {
221        mDefaultWidth = width;
222        final int threshold = (int) (width * SEARCH_DISTANCE);
223        mProximityThreshold = threshold * threshold;
224    }
225
226    /**
227     * Returns the total height of the keyboard
228     * @return the total height of the keyboard
229     */
230    public int getHeight() {
231        return mTotalHeight;
232    }
233
234    public void setHeight(int height) {
235        mTotalHeight = height;
236    }
237
238    public int getMinWidth() {
239        return mMinWidth;
240    }
241
242    public void setMinWidth(int minWidth) {
243        mMinWidth = minWidth;
244    }
245
246    public int getDisplayHeight() {
247        return mDisplayHeight;
248    }
249
250    public int getDisplayWidth() {
251        return mDisplayWidth;
252    }
253
254    public int getKeyboardHeight() {
255        return mKeyboardHeight;
256    }
257
258    public void setKeyboardHeight(int height) {
259        mKeyboardHeight = height;
260    }
261
262    public int getPopupKeyboardResId() {
263        return mPopupKeyboardResId;
264    }
265
266    public void setPopupKeyboardResId(int resId) {
267        mPopupKeyboardResId = resId;
268    }
269
270    public int getMaxPopupKeyboardColumn() {
271        return mMaxPopupColumn;
272    }
273
274    public void setMaxPopupKeyboardColumn(int column) {
275        mMaxPopupColumn = column;
276    }
277
278    public List<Key> getShiftKeys() {
279        return mShiftKeys;
280    }
281
282    public Map<Key, Drawable> getShiftedIcons() {
283        return mShiftedIcons;
284    }
285
286    public void enableShiftLock() {
287        for (final Key key : getShiftKeys()) {
288            mShiftLockEnabled.add(key);
289            mNormalShiftIcons.put(key, key.getIcon());
290        }
291    }
292
293    public boolean isShiftLockEnabled(Key key) {
294        return mShiftLockEnabled.contains(key);
295    }
296
297    public boolean setShiftLocked(boolean newShiftLockState) {
298        final Map<Key, Drawable> shiftedIcons = getShiftedIcons();
299        for (final Key key : getShiftKeys()) {
300            key.setHighlightOn(newShiftLockState);
301            key.setIcon(newShiftLockState ? shiftedIcons.get(key) : mNormalShiftIcons.get(key));
302        }
303        mShiftState.setShiftLocked(newShiftLockState);
304        return true;
305    }
306
307    public boolean isShiftLocked() {
308        return mShiftState.isShiftLocked();
309    }
310
311    public boolean setShifted(boolean newShiftState) {
312        final Map<Key, Drawable> shiftedIcons = getShiftedIcons();
313        for (final Key key : getShiftKeys()) {
314            if (!newShiftState && !mShiftState.isShiftLocked()) {
315                key.setIcon(mNormalShiftIcons.get(key));
316            } else if (newShiftState && !mShiftState.isShiftedOrShiftLocked()) {
317                key.setIcon(shiftedIcons.get(key));
318            }
319        }
320        return mShiftState.setShifted(newShiftState);
321    }
322
323    public boolean isShiftedOrShiftLocked() {
324        return mShiftState.isShiftedOrShiftLocked();
325    }
326
327    public void setAutomaticTemporaryUpperCase() {
328        setShifted(true);
329        mShiftState.setAutomaticTemporaryUpperCase();
330    }
331
332    public boolean isAutomaticTemporaryUpperCase() {
333        return isAlphaKeyboard() && mShiftState.isAutomaticTemporaryUpperCase();
334    }
335
336    public boolean isManualTemporaryUpperCase() {
337        return isAlphaKeyboard() && mShiftState.isManualTemporaryUpperCase();
338    }
339
340    public boolean isManualTemporaryUpperCaseFromAuto() {
341        return isAlphaKeyboard() && mShiftState.isManualTemporaryUpperCaseFromAuto();
342    }
343
344    public KeyboardShiftState getKeyboardShiftState() {
345        return mShiftState;
346    }
347
348    public boolean isAlphaKeyboard() {
349        return mId.isAlphabetKeyboard();
350    }
351
352    public boolean isPhoneKeyboard() {
353        return mId.isPhoneKeyboard();
354    }
355
356    public boolean isNumberKeyboard() {
357        return mId.isNumberKeyboard();
358    }
359
360    public CharSequence adjustLabelCase(CharSequence label) {
361        if (isShiftedOrShiftLocked() && !TextUtils.isEmpty(label) && label.length() < 3
362                && Character.isLowerCase(label.charAt(0))) {
363            return label.toString().toUpperCase(mId.mLocale);
364        }
365        return label;
366    }
367
368    // TODO: Move this function to ProximityInfo and make this private.
369    public void computeNearestNeighbors() {
370        // Round-up so we don't have any pixels outside the grid
371        mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH;
372        mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT;
373        mGridNeighbors = new int[GRID_SIZE][];
374        final int[] indices = new int[mKeys.size()];
375        final int gridWidth = GRID_WIDTH * mCellWidth;
376        final int gridHeight = GRID_HEIGHT * mCellHeight;
377        final int threshold = mProximityThreshold;
378        for (int x = 0; x < gridWidth; x += mCellWidth) {
379            for (int y = 0; y < gridHeight; y += mCellHeight) {
380                final int centerX = x + mCellWidth / 2;
381                final int centerY = y + mCellHeight / 2;
382                int count = 0;
383                for (int i = 0; i < mKeys.size(); i++) {
384                    final Key key = mKeys.get(i);
385                    if (key.squaredDistanceToEdge(centerX, centerY) < threshold)
386                        indices[count++] = i;
387                }
388                final int[] cell = new int[count];
389                System.arraycopy(indices, 0, cell, 0, count);
390                mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell;
391            }
392        }
393        mProximityInfo.setProximityInfo(mGridNeighbors, getMinWidth(), getHeight(), mKeys);
394    }
395
396    /**
397     * Returns the indices of the keys that are closest to the given point.
398     * @param x the x-coordinate of the point
399     * @param y the y-coordinate of the point
400     * @return the array of integer indices for the nearest keys to the given point. If the given
401     * point is out of range, then an array of size zero is returned.
402     */
403    public int[] getNearestKeys(int x, int y) {
404        if (mGridNeighbors == null) computeNearestNeighbors();
405        if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) {
406            int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth);
407            if (index < GRID_SIZE) {
408                return mGridNeighbors[index];
409            }
410        }
411        return EMPTY_INT_ARRAY;
412    }
413
414    /**
415     * Compute the most common key width in order to use it as proximity key detection threshold.
416     *
417     * @return The most common key width in the keyboard
418     */
419    public int getMostCommonKeyWidth() {
420        if (mMostCommonKeyWidth == 0) {
421            final HashMap<Integer, Integer> histogram = new HashMap<Integer, Integer>();
422            int maxCount = 0;
423            int mostCommonWidth = 0;
424            for (final Key key : mKeys) {
425                final Integer width = key.mWidth + key.mGap;
426                Integer count = histogram.get(width);
427                if (count == null)
428                    count = 0;
429                histogram.put(width, ++count);
430                if (count > maxCount) {
431                    maxCount = count;
432                    mostCommonWidth = width;
433                }
434            }
435            mMostCommonKeyWidth = mostCommonWidth;
436        }
437        return mMostCommonKeyWidth;
438    }
439
440    /**
441     * Return true if spacebar needs showing preview even when "popup on keypress" is off.
442     * @param keyIndex index of the pressing key
443     * @return true if spacebar needs showing preview
444     */
445    public boolean needSpacebarPreview(int keyIndex) {
446        return false;
447    }
448
449    private void loadKeyboard(Context context, int xmlLayoutResId) {
450        try {
451            KeyboardParser parser = new KeyboardParser(this, context);
452            parser.parseKeyboard(xmlLayoutResId);
453            // mMinWidth is the width of this keyboard which is maximum width of row.
454            mMinWidth = parser.getMaxRowWidth();
455            mTotalHeight = parser.getTotalHeight();
456        } catch (XmlPullParserException e) {
457            Log.w(TAG, "keyboard XML parse error: " + e);
458            throw new IllegalArgumentException(e);
459        } catch (IOException e) {
460            Log.w(TAG, "keyboard XML parse error: " + e);
461            throw new RuntimeException(e);
462        }
463    }
464
465    public static void setDefaultBounds(Drawable drawable)  {
466        if (drawable != null)
467            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
468                    drawable.getIntrinsicHeight());
469    }
470}
471