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