MoreKeySpec.java revision f70bcf3d323b13b60c0567c69768ed986647f86a
1/*
2 * Copyright (C) 2012 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.internal;
18
19import android.text.TextUtils;
20
21import com.android.inputmethod.keyboard.Key;
22import com.android.inputmethod.latin.Constants;
23import com.android.inputmethod.latin.LatinImeLogger;
24import com.android.inputmethod.latin.utils.CollectionUtils;
25import com.android.inputmethod.latin.utils.StringUtils;
26
27import java.util.ArrayList;
28import java.util.Arrays;
29import java.util.Locale;
30
31/**
32 * The more key specification object. The more keys are an array of {@link MoreKeySpec}.
33 *
34 * The more keys specification is comma separated "key specification" each of which represents one
35 * "more key".
36 * The key specification might have label or string resource reference in it. These references are
37 * expanded before parsing comma.
38 * Special character, comma ',' backslash '\' can be escaped by '\' character.
39 * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
40 * as well.
41 */
42// TODO: Should extend the key specification object.
43public final class MoreKeySpec {
44    public final int mCode;
45    public final String mLabel;
46    public final String mOutputText;
47    public final int mIconId;
48
49    public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, final Locale locale,
50            final KeyboardCodesSet codesSet) {
51        if (TextUtils.isEmpty(moreKeySpec)) {
52            throw new KeySpecParser.KeySpecParserError("Empty more key spec");
53        }
54        mLabel = StringUtils.toUpperCaseOfStringForLocale(
55                KeySpecParser.getLabel(moreKeySpec), needsToUpperCase, locale);
56        final int code = StringUtils.toUpperCaseOfCodeForLocale(
57                KeySpecParser.getCode(moreKeySpec, codesSet), needsToUpperCase, locale);
58        if (code == Constants.CODE_UNSPECIFIED) {
59            // Some letter, for example German Eszett (U+00DF: "ß"), has multiple characters
60            // upper case representation ("SS").
61            mCode = Constants.CODE_OUTPUT_TEXT;
62            mOutputText = mLabel;
63        } else {
64            mCode = code;
65            mOutputText = StringUtils.toUpperCaseOfStringForLocale(
66                    KeySpecParser.getOutputText(moreKeySpec), needsToUpperCase, locale);
67        }
68        mIconId = KeySpecParser.getIconId(moreKeySpec);
69    }
70
71    public Key buildKey(final int x, final int y, final int labelFlags,
72            final KeyboardParams params) {
73        return new Key(mLabel, mIconId, mCode, mOutputText, null /* hintLabel */, labelFlags,
74                Key.BACKGROUND_TYPE_NORMAL, x, y, params.mDefaultKeyWidth, params.mDefaultRowHeight,
75                params.mHorizontalGap, params.mVerticalGap);
76    }
77
78    @Override
79    public int hashCode() {
80        int hashCode = 1;
81        hashCode = 31 + mCode;
82        hashCode = hashCode * 31 + mIconId;
83        hashCode = hashCode * 31 + (mLabel == null ? 0 : mLabel.hashCode());
84        hashCode = hashCode * 31 + (mOutputText == null ? 0 : mOutputText.hashCode());
85        return hashCode;
86    }
87
88    @Override
89    public boolean equals(final Object o) {
90        if (this == o) return true;
91        if (o instanceof MoreKeySpec) {
92            final MoreKeySpec other = (MoreKeySpec)o;
93            return mCode == other.mCode
94                    && mIconId == other.mIconId
95                    && TextUtils.equals(mLabel, other.mLabel)
96                    && TextUtils.equals(mOutputText, other.mOutputText);
97        }
98        return false;
99    }
100
101    @Override
102    public String toString() {
103        final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel
104                : KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId));
105        final String output = (mCode == Constants.CODE_OUTPUT_TEXT ? mOutputText
106                : Constants.printableCode(mCode));
107        if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) {
108            return output;
109        } else {
110            return label + "|" + output;
111        }
112    }
113
114    private static final boolean DEBUG = LatinImeLogger.sDBG;
115    // Constants for parsing.
116    private static final char COMMA = Constants.CODE_COMMA;
117    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
118    private static final String ADDITIONAL_MORE_KEY_MARKER =
119            StringUtils.newSingleCodePointString(Constants.CODE_PERCENT);
120
121    /**
122     * Split the text containing multiple key specifications separated by commas into an array of
123     * key specifications.
124     * A key specification can contain a character escaped by the backslash character, including a
125     * comma character.
126     * Note that an empty key specification will be eliminated from the result array.
127     *
128     * @param text the text containing multiple key specifications.
129     * @return an array of key specification text. Null if the specified <code>text</code> is empty
130     * or has no key specifications.
131     */
132    public static String[] splitKeySpecs(final String text) {
133        if (TextUtils.isEmpty(text)) {
134            return null;
135        }
136        final int size = text.length();
137        // Optimization for one-letter key specification.
138        if (size == 1) {
139            return text.charAt(0) == COMMA ? null : new String[] { text };
140        }
141
142        ArrayList<String> list = null;
143        int start = 0;
144        // The characters in question in this loop are COMMA and BACKSLASH. These characters never
145        // match any high or low surrogate character. So it is OK to iterate through with char
146        // index.
147        for (int pos = 0; pos < size; pos++) {
148            final char c = text.charAt(pos);
149            if (c == COMMA) {
150                // Skip empty entry.
151                if (pos - start > 0) {
152                    if (list == null) {
153                        list = CollectionUtils.newArrayList();
154                    }
155                    list.add(text.substring(start, pos));
156                }
157                // Skip comma
158                start = pos + 1;
159            } else if (c == BACKSLASH) {
160                // Skip escape character and escaped character.
161                pos++;
162            }
163        }
164        final String remain = (size - start > 0) ? text.substring(start) : null;
165        if (list == null) {
166            return remain != null ? new String[] { remain } : null;
167        }
168        if (remain != null) {
169            list.add(remain);
170        }
171        return list.toArray(new String[list.size()]);
172    }
173
174    private static final String[] EMPTY_STRING_ARRAY = new String[0];
175
176    private static String[] filterOutEmptyString(final String[] array) {
177        if (array == null) {
178            return EMPTY_STRING_ARRAY;
179        }
180        ArrayList<String> out = null;
181        for (int i = 0; i < array.length; i++) {
182            final String entry = array[i];
183            if (TextUtils.isEmpty(entry)) {
184                if (out == null) {
185                    out = CollectionUtils.arrayAsList(array, 0, i);
186                }
187            } else if (out != null) {
188                out.add(entry);
189            }
190        }
191        if (out == null) {
192            return array;
193        }
194        return out.toArray(new String[out.size()]);
195    }
196
197    public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs,
198            final String[] additionalMoreKeySpecs) {
199        final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
200        final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
201        final int moreKeysCount = moreKeys.length;
202        final int additionalCount = additionalMoreKeys.length;
203        ArrayList<String> out = null;
204        int additionalIndex = 0;
205        for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) {
206            final String moreKeySpec = moreKeys[moreKeyIndex];
207            if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) {
208                if (additionalIndex < additionalCount) {
209                    // Replace '%' marker with additional more key specification.
210                    final String additionalMoreKey = additionalMoreKeys[additionalIndex];
211                    if (out != null) {
212                        out.add(additionalMoreKey);
213                    } else {
214                        moreKeys[moreKeyIndex] = additionalMoreKey;
215                    }
216                    additionalIndex++;
217                } else {
218                    // Filter out excessive '%' marker.
219                    if (out == null) {
220                        out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex);
221                    }
222                }
223            } else {
224                if (out != null) {
225                    out.add(moreKeySpec);
226                }
227            }
228        }
229        if (additionalCount > 0 && additionalIndex == 0) {
230            // No '%' marker is found in more keys.
231            // Insert all additional more keys to the head of more keys.
232            if (DEBUG && out != null) {
233                throw new RuntimeException("Internal logic error:"
234                        + " moreKeys=" + Arrays.toString(moreKeys)
235                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
236            }
237            out = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
238            for (int i = 0; i < moreKeysCount; i++) {
239                out.add(moreKeys[i]);
240            }
241        } else if (additionalIndex < additionalCount) {
242            // The number of '%' markers are less than additional more keys.
243            // Append remained additional more keys to the tail of more keys.
244            if (DEBUG && out != null) {
245                throw new RuntimeException("Internal logic error:"
246                        + " moreKeys=" + Arrays.toString(moreKeys)
247                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
248            }
249            out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount);
250            for (int i = additionalIndex; i < additionalCount; i++) {
251                out.add(additionalMoreKeys[additionalIndex]);
252            }
253        }
254        if (out == null && moreKeysCount > 0) {
255            return moreKeys;
256        } else if (out != null && out.size() > 0) {
257            return out.toArray(new String[out.size()]);
258        } else {
259            return null;
260        }
261    }
262
263    public static int getIntValue(final String[] moreKeys, final String key,
264            final int defaultValue) {
265        if (moreKeys == null) {
266            return defaultValue;
267        }
268        final int keyLen = key.length();
269        boolean foundValue = false;
270        int value = defaultValue;
271        for (int i = 0; i < moreKeys.length; i++) {
272            final String moreKeySpec = moreKeys[i];
273            if (moreKeySpec == null || !moreKeySpec.startsWith(key)) {
274                continue;
275            }
276            moreKeys[i] = null;
277            try {
278                if (!foundValue) {
279                    value = Integer.parseInt(moreKeySpec.substring(keyLen));
280                    foundValue = true;
281                }
282            } catch (NumberFormatException e) {
283                throw new RuntimeException(
284                        "integer should follow after " + key + ": " + moreKeySpec);
285            }
286        }
287        return value;
288    }
289
290    public static boolean getBooleanValue(final String[] moreKeys, final String key) {
291        if (moreKeys == null) {
292            return false;
293        }
294        boolean value = false;
295        for (int i = 0; i < moreKeys.length; i++) {
296            final String moreKeySpec = moreKeys[i];
297            if (moreKeySpec == null || !moreKeySpec.equals(key)) {
298                continue;
299            }
300            moreKeys[i] = null;
301            value = true;
302        }
303        return value;
304    }
305}
306