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