19b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka/* 28632bff2d5a8e1160989008dea6eff4b94b065ddTadashi G. Takaoka * Copyright (C) 2010 The Android Open Source Project 39b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka * 48aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * Licensed under the Apache License, Version 2.0 (the "License"); 58aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * you may not use this file except in compliance with the License. 68aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * You may obtain a copy of the License at 79b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka * 88aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * http://www.apache.org/licenses/LICENSE-2.0 99b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka * 109b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka * Unless required by applicable law or agreed to in writing, software 118aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * distributed under the License is distributed on an "AS IS" BASIS, 128aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 138aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * See the License for the specific language governing permissions and 148aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * limitations under the License. 159b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka */ 169b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka 1772934bd5967d0127f71fd4d66158b18b4e6ceefeTadashi G. Takaokapackage com.android.inputmethod.keyboard.internal; 189b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka 199342484e8d573a40f470b6a593df31c602fa4076Ken Wakasaimport static com.android.inputmethod.latin.common.Constants.CODE_OUTPUT_TEXT; 209342484e8d573a40f470b6a593df31c602fa4076Ken Wakasaimport static com.android.inputmethod.latin.common.Constants.CODE_UNSPECIFIED; 217ab7f66c2d0f4a0b2e29be718b310ccaf368a4f4Tadashi G. Takaoka 229342484e8d573a40f470b6a593df31c602fa4076Ken Wakasaimport com.android.inputmethod.latin.common.Constants; 234beeb9253a06482299e0c67467531d30436a02fcJean Chalardimport com.android.inputmethod.latin.common.StringUtils; 249b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka 259d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaokaimport javax.annotation.Nonnull; 269d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaokaimport javax.annotation.Nullable; 279d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka 289b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka/** 29e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * The string parser of the key specification. 30e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * 31e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * Each key specification is one of the following: 32e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Label optionally followed by keyOutputText (keyLabel|keyOutputText). 33e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Label optionally followed by code point (keyLabel|!code/code_name). 34e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Icon followed by keyOutputText (!icon/icon_name|keyOutputText). 35e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Icon followed by code point (!icon/icon_name|!code/code_name). 36e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * Label and keyOutputText are one of the following: 37e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Literal string. 38e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Label reference represented by (!text/label_name), see {@link KeyboardTextsSet}. 39e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - String resource reference represented by (!text/resource_name), see {@link KeyboardTextsSet}. 40e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * Icon is represented by (!icon/icon_name), see {@link KeyboardIconsSet}. 41e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * Code is one of the following: 42e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Code point presented by hexadecimal string prefixed with "0x" 43e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Code reference represented by (!code/code_name), see {@link KeyboardCodesSet}. 440086861d5a2d16f86e2130ee9a7ec46ca533fadbTadashi G. Takaoka * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character. 45e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)} 46e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * as well. 479b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka */ 489684b33b69a62a058c767786ae6a23b809d27385Tadashi G. Takaoka// TODO: Rename to KeySpec and make this class to the key specification object. 49a28a05e971cc242b338331a3b78276fa95188d19Tadashi G. Takaokapublic final class KeySpecParser { 500086861d5a2d16f86e2130ee9a7ec46ca533fadbTadashi G. Takaoka // Constants for parsing. 51e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka private static final char BACKSLASH = Constants.CODE_BACKSLASH; 52e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka private static final char VERTICAL_BAR = Constants.CODE_VERTICAL_BAR; 53ed3bac91f242850c6d1833a5f8981b9cc208c5ddTadashi G. Takaoka private static final String PREFIX_HEX = "0x"; 549b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka 55e01d272603f3643ce613e61dd3204379f4f4fb73Tadashi G. Takaoka private KeySpecParser() { 569b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka // Intentional empty constructor for utility class. 579b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 589b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka 599d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka private static boolean hasIcon(@Nonnull final String keySpec) { 60e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON); 619b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 629b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka 639d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka private static boolean hasCode(@Nonnull final String keySpec, final int labelEnd) { 644a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka if (labelEnd <= 0 || labelEnd + 1 >= keySpec.length()) { 654a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka return false; 664a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka } 674a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka if (keySpec.startsWith(KeyboardCodesSet.PREFIX_CODE, labelEnd + 1)) { 684a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka return true; 694a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka } 704a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka // This is a workaround to have a key that has a supplementary code point. We can't put a 714a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka // string in resource as a XML entity of a supplementary code point or a surrogate pair. 724a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka if (keySpec.startsWith(PREFIX_HEX, labelEnd + 1)) { 739b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka return true; 749b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 759b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka return false; 769b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 779b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka 789d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka @Nonnull 799d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka private static String parseEscape(@Nonnull final String text) { 80dbed20ad8d48e9b42b52c522c54bfd9e62553f7aTadashi G. Takaoka if (text.indexOf(BACKSLASH) < 0) { 819b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka return text; 8223bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka } 839b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka final int length = text.length(); 849b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka final StringBuilder sb = new StringBuilder(); 859b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka for (int pos = 0; pos < length; pos++) { 869b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka final char c = text.charAt(pos); 87dbed20ad8d48e9b42b52c522c54bfd9e62553f7aTadashi G. Takaoka if (c == BACKSLASH && pos + 1 < length) { 88e01d272603f3643ce613e61dd3204379f4f4fb73Tadashi G. Takaoka // Skip escape char 89e01d272603f3643ce613e61dd3204379f4f4fb73Tadashi G. Takaoka pos++; 90e01d272603f3643ce613e61dd3204379f4f4fb73Tadashi G. Takaoka sb.append(text.charAt(pos)); 919b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } else { 929b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka sb.append(c); 939b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 949b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 959b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka return sb.toString(); 969b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 979b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka 989d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka private static int indexOfLabelEnd(@Nonnull final String keySpec) { 99f7d8b8fcbcfe137c1c25ca7ea5bd83f856f06b9eTadashi G. Takaoka final int length = keySpec.length(); 100bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka if (keySpec.indexOf(BACKSLASH) < 0) { 101bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka final int labelEnd = keySpec.indexOf(VERTICAL_BAR); 102bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka if (labelEnd == 0) { 103f7d8b8fcbcfe137c1c25ca7ea5bd83f856f06b9eTadashi G. Takaoka if (length == 1) { 104f7d8b8fcbcfe137c1c25ca7ea5bd83f856f06b9eTadashi G. Takaoka // Treat a sole vertical bar as a special case of key label. 105f7d8b8fcbcfe137c1c25ca7ea5bd83f856f06b9eTadashi G. Takaoka return -1; 106f7d8b8fcbcfe137c1c25ca7ea5bd83f856f06b9eTadashi G. Takaoka } 107bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka throw new KeySpecParserError("Empty label"); 10823bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka } 109bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka return labelEnd; 1109b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 111bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka for (int pos = 0; pos < length; pos++) { 112e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka final char c = keySpec.charAt(pos); 113dbed20ad8d48e9b42b52c522c54bfd9e62553f7aTadashi G. Takaoka if (c == BACKSLASH && pos + 1 < length) { 114e01d272603f3643ce613e61dd3204379f4f4fb73Tadashi G. Takaoka // Skip escape char 1159b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka pos++; 116dbed20ad8d48e9b42b52c522c54bfd9e62553f7aTadashi G. Takaoka } else if (c == VERTICAL_BAR) { 1179b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka return pos; 1189b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 1199b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 1209b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka return -1; 1219b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 1229b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka 1239d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka @Nonnull 1249d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka private static String getBeforeLabelEnd(@Nonnull final String keySpec, final int labelEnd) { 125bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka return (labelEnd < 0) ? keySpec : keySpec.substring(0, labelEnd); 126bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka } 127bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka 1289d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka @Nonnull 1299d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka private static String getAfterLabelEnd(@Nonnull final String keySpec, final int labelEnd) { 130bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka return keySpec.substring(labelEnd + /* VERTICAL_BAR */1); 131bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka } 132bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka 1339d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka private static void checkDoubleLabelEnd(@Nonnull final String keySpec, final int labelEnd) { 134bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka if (indexOfLabelEnd(getAfterLabelEnd(keySpec, labelEnd)) < 0) { 135bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka return; 136bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka } 137bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec); 138bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka } 139bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka 1409d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka @Nullable 1419d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka public static String getLabel(@Nullable final String keySpec) { 142d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka if (keySpec == null) { 143d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. 144d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka return null; 145d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka } 146e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka if (hasIcon(keySpec)) { 1479b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka return null; 14823bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka } 149bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka final int labelEnd = indexOfLabelEnd(keySpec); 150bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka final String label = parseEscape(getBeforeLabelEnd(keySpec, labelEnd)); 151e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka if (label.isEmpty()) { 152e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka throw new KeySpecParserError("Empty label: " + keySpec); 15323bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka } 1549b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka return label; 1559b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 1569b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka 1579d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka @Nullable 1589d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka private static String getOutputTextInternal(@Nonnull final String keySpec, final int labelEnd) { 159bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka if (labelEnd <= 0) { 160c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka return null; 161c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka } 162bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka checkDoubleLabelEnd(keySpec, labelEnd); 163bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka return parseEscape(getAfterLabelEnd(keySpec, labelEnd)); 164c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka } 165c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka 1669d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka @Nullable 1679d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka public static String getOutputText(@Nullable final String keySpec) { 168d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka if (keySpec == null) { 169d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. 170d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka return null; 171d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka } 172bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka final int labelEnd = indexOfLabelEnd(keySpec); 173bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka if (hasCode(keySpec, labelEnd)) { 1749b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka return null; 17523bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka } 176bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka final String outputText = getOutputTextInternal(keySpec, labelEnd); 177c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka if (outputText != null) { 178cc8c8b99bd0463f5977dea82f5e2379ea1dd4e73Tadashi G. Takaoka if (StringUtils.codePointCount(outputText) == 1) { 179c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka // If output text is one code point, it should be treated as a code. 180c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka // See {@link #getCode(Resources, String)}. 181c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka return null; 18223bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka } 183e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka if (outputText.isEmpty()) { 184e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka throw new KeySpecParserError("Empty outputText: " + keySpec); 18523bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka } 186e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka return outputText; 1879b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 188e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka final String label = getLabel(keySpec); 18923bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka if (label == null) { 190e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka throw new KeySpecParserError("Empty label: " + keySpec); 19123bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka } 192c4f71668d7b8203dc66f0f04c089a363189eb4ceTadashi G. Takaoka // Code is automatically generated for one letter label. See {@link getCode()}. 193cc8c8b99bd0463f5977dea82f5e2379ea1dd4e73Tadashi G. Takaoka return (StringUtils.codePointCount(label) == 1) ? null : label; 1949b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 1959b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka 1969d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka public static int getCode(@Nullable final String keySpec) { 197d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka if (keySpec == null) { 198d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. 199d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka return CODE_UNSPECIFIED; 200d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka } 201bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka final int labelEnd = indexOfLabelEnd(keySpec); 202bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka if (hasCode(keySpec, labelEnd)) { 203bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka checkDoubleLabelEnd(keySpec, labelEnd); 2047ae6721ffad1e79ee446de87d13f18a27619830bTadashi G. Takaoka return parseCode(getAfterLabelEnd(keySpec, labelEnd), CODE_UNSPECIFIED); 2059b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 206bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka final String outputText = getOutputTextInternal(keySpec, labelEnd); 207c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka if (outputText != null) { 208c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka // If output text is one code point, it should be treated as a code. 209c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka // See {@link #getOutputText(String)}. 210cc8c8b99bd0463f5977dea82f5e2379ea1dd4e73Tadashi G. Takaoka if (StringUtils.codePointCount(outputText) == 1) { 211c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka return outputText.codePointAt(0); 212c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka } 213240871ecafde7834ebb4270cd7758fc904a5f3a7Tadashi G. Takaoka return CODE_OUTPUT_TEXT; 21423bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka } 215e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka final String label = getLabel(keySpec); 216bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka if (label == null) { 217bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka throw new KeySpecParserError("Empty label: " + keySpec); 21823bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka } 219bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka // Code is automatically generated for one letter label. 220bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka return (StringUtils.codePointCount(label) == 1) ? label.codePointAt(0) : CODE_OUTPUT_TEXT; 2219b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 2229b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka 2239d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka public static int parseCode(@Nullable final String text, final int defaultCode) { 224bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka if (text == null) { 2254a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka return defaultCode; 226bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka } 2273b4eb03fa171ab42ad4f38abcfa5184c5362e5aeTadashi G. Takaoka if (text.startsWith(KeyboardCodesSet.PREFIX_CODE)) { 2287ae6721ffad1e79ee446de87d13f18a27619830bTadashi G. Takaoka return KeyboardCodesSet.getCode(text.substring(KeyboardCodesSet.PREFIX_CODE.length())); 229bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka } 2304a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka // This is a workaround to have a key that has a supplementary code point. We can't put a 2314a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka // string in resource as a XML entity of a supplementary code point or a surrogate pair. 232bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka if (text.startsWith(PREFIX_HEX)) { 233ed3bac91f242850c6d1833a5f8981b9cc208c5ddTadashi G. Takaoka return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16); 234ed3bac91f242850c6d1833a5f8981b9cc208c5ddTadashi G. Takaoka } 2354a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka return defaultCode; 236ed3bac91f242850c6d1833a5f8981b9cc208c5ddTadashi G. Takaoka } 237ed3bac91f242850c6d1833a5f8981b9cc208c5ddTadashi G. Takaoka 2389d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka public static int getIconId(@Nullable final String keySpec) { 239d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka if (keySpec == null) { 240d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. 241d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka return KeyboardIconsSet.ICON_UNDEFINED; 242d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka } 243bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka if (!hasIcon(keySpec)) { 244bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka return KeyboardIconsSet.ICON_UNDEFINED; 245bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka } 246bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka final int labelEnd = indexOfLabelEnd(keySpec); 247bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka final String iconName = getBeforeLabelEnd(keySpec, labelEnd) 248bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka .substring(KeyboardIconsSet.PREFIX_ICON.length()); 249bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka return KeyboardIconsSet.getIconId(iconName); 2509b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka } 2519b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka 25215c99e9f1a7776b95325d36cf9e38b6d674e483bTadashi G. Takaoka @SuppressWarnings("serial") 253a28a05e971cc242b338331a3b78276fa95188d19Tadashi G. Takaoka public static final class KeySpecParserError extends RuntimeException { 25435ff94547c16c84c5b6fafdae0b4a683be782b97Tadashi G. Takaoka public KeySpecParserError(final String message) { 25515c99e9f1a7776b95325d36cf9e38b6d674e483bTadashi G. Takaoka super(message); 2562fe68b9616ebdeb24daf043fbc590ea6a11f10a0Tadashi G. Takaoka } 2572fe68b9616ebdeb24daf043fbc590ea6a11f10a0Tadashi G. Takaoka } 2589b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka} 259