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