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