1 /* 2 * Copyright (C) 2013 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 17 package com.android.inputmethod.event; 18 19 import android.text.TextUtils; 20 import android.util.SparseIntArray; 21 22 import com.android.inputmethod.latin.common.Constants; 23 24 import java.text.Normalizer; 25 import java.util.ArrayList; 26 27 import javax.annotation.Nonnull; 28 29 /** 30 * A combiner that handles dead keys. 31 */ 32 public class DeadKeyCombiner implements Combiner { 33 34 private static class Data { 35 // This class data taken from KeyCharacterMap.java. 36 37 /* Characters used to display placeholders for dead keys. */ 38 private static final int ACCENT_ACUTE = '\u00B4'; 39 private static final int ACCENT_BREVE = '\u02D8'; 40 private static final int ACCENT_CARON = '\u02C7'; 41 private static final int ACCENT_CEDILLA = '\u00B8'; 42 private static final int ACCENT_CIRCUMFLEX = '\u02C6'; 43 private static final int ACCENT_COMMA_ABOVE = '\u1FBD'; 44 private static final int ACCENT_COMMA_ABOVE_RIGHT = '\u02BC'; 45 private static final int ACCENT_DOT_ABOVE = '\u02D9'; 46 private static final int ACCENT_DOT_BELOW = Constants.CODE_PERIOD; // approximate 47 private static final int ACCENT_DOUBLE_ACUTE = '\u02DD'; 48 private static final int ACCENT_GRAVE = '\u02CB'; 49 private static final int ACCENT_HOOK_ABOVE = '\u02C0'; 50 private static final int ACCENT_HORN = Constants.CODE_SINGLE_QUOTE; // approximate 51 private static final int ACCENT_MACRON = '\u00AF'; 52 private static final int ACCENT_MACRON_BELOW = '\u02CD'; 53 private static final int ACCENT_OGONEK = '\u02DB'; 54 private static final int ACCENT_REVERSED_COMMA_ABOVE = '\u02BD'; 55 private static final int ACCENT_RING_ABOVE = '\u02DA'; 56 private static final int ACCENT_STROKE = Constants.CODE_DASH; // approximate 57 private static final int ACCENT_TILDE = '\u02DC'; 58 private static final int ACCENT_TURNED_COMMA_ABOVE = '\u02BB'; 59 private static final int ACCENT_UMLAUT = '\u00A8'; 60 private static final int ACCENT_VERTICAL_LINE_ABOVE = '\u02C8'; 61 private static final int ACCENT_VERTICAL_LINE_BELOW = '\u02CC'; 62 63 /* Legacy dead key display characters used in previous versions of the API (before L) 64 * We still support these characters by mapping them to their non-legacy version. */ 65 private static final int ACCENT_GRAVE_LEGACY = Constants.CODE_GRAVE_ACCENT; 66 private static final int ACCENT_CIRCUMFLEX_LEGACY = Constants.CODE_CIRCUMFLEX_ACCENT; 67 private static final int ACCENT_TILDE_LEGACY = Constants.CODE_TILDE; 68 69 /** 70 * Maps Unicode combining diacritical to display-form dead key. 71 */ 72 static final SparseIntArray sCombiningToAccent = new SparseIntArray(); 73 static final SparseIntArray sAccentToCombining = new SparseIntArray(); 74 static { 75 // U+0300: COMBINING GRAVE ACCENT 76 addCombining('\u0300', ACCENT_GRAVE); 77 // U+0301: COMBINING ACUTE ACCENT 78 addCombining('\u0301', ACCENT_ACUTE); 79 // U+0302: COMBINING CIRCUMFLEX ACCENT 80 addCombining('\u0302', ACCENT_CIRCUMFLEX); 81 // U+0303: COMBINING TILDE 82 addCombining('\u0303', ACCENT_TILDE); 83 // U+0304: COMBINING MACRON 84 addCombining('\u0304', ACCENT_MACRON); 85 // U+0306: COMBINING BREVE 86 addCombining('\u0306', ACCENT_BREVE); 87 // U+0307: COMBINING DOT ABOVE 88 addCombining('\u0307', ACCENT_DOT_ABOVE); 89 // U+0308: COMBINING DIAERESIS 90 addCombining('\u0308', ACCENT_UMLAUT); 91 // U+0309: COMBINING HOOK ABOVE 92 addCombining('\u0309', ACCENT_HOOK_ABOVE); 93 // U+030A: COMBINING RING ABOVE 94 addCombining('\u030A', ACCENT_RING_ABOVE); 95 // U+030B: COMBINING DOUBLE ACUTE ACCENT 96 addCombining('\u030B', ACCENT_DOUBLE_ACUTE); 97 // U+030C: COMBINING CARON 98 addCombining('\u030C', ACCENT_CARON); 99 // U+030D: COMBINING VERTICAL LINE ABOVE 100 addCombining('\u030D', ACCENT_VERTICAL_LINE_ABOVE); 101 // U+030E: COMBINING DOUBLE VERTICAL LINE ABOVE 102 //addCombining('\u030E', ACCENT_DOUBLE_VERTICAL_LINE_ABOVE); 103 // U+030F: COMBINING DOUBLE GRAVE ACCENT 104 //addCombining('\u030F', ACCENT_DOUBLE_GRAVE); 105 // U+0310: COMBINING CANDRABINDU 106 //addCombining('\u0310', ACCENT_CANDRABINDU); 107 // U+0311: COMBINING INVERTED BREVE 108 //addCombining('\u0311', ACCENT_INVERTED_BREVE); 109 // U+0312: COMBINING TURNED COMMA ABOVE 110 addCombining('\u0312', ACCENT_TURNED_COMMA_ABOVE); 111 // U+0313: COMBINING COMMA ABOVE 112 addCombining('\u0313', ACCENT_COMMA_ABOVE); 113 // U+0314: COMBINING REVERSED COMMA ABOVE 114 addCombining('\u0314', ACCENT_REVERSED_COMMA_ABOVE); 115 // U+0315: COMBINING COMMA ABOVE RIGHT 116 addCombining('\u0315', ACCENT_COMMA_ABOVE_RIGHT); 117 // U+031B: COMBINING HORN 118 addCombining('\u031B', ACCENT_HORN); 119 // U+0323: COMBINING DOT BELOW 120 addCombining('\u0323', ACCENT_DOT_BELOW); 121 // U+0326: COMBINING COMMA BELOW 122 //addCombining('\u0326', ACCENT_COMMA_BELOW); 123 // U+0327: COMBINING CEDILLA 124 addCombining('\u0327', ACCENT_CEDILLA); 125 // U+0328: COMBINING OGONEK 126 addCombining('\u0328', ACCENT_OGONEK); 127 // U+0329: COMBINING VERTICAL LINE BELOW 128 addCombining('\u0329', ACCENT_VERTICAL_LINE_BELOW); 129 // U+0331: COMBINING MACRON BELOW 130 addCombining('\u0331', ACCENT_MACRON_BELOW); 131 // U+0335: COMBINING SHORT STROKE OVERLAY 132 addCombining('\u0335', ACCENT_STROKE); 133 // U+0342: COMBINING GREEK PERISPOMENI 134 //addCombining('\u0342', ACCENT_PERISPOMENI); 135 // U+0344: COMBINING GREEK DIALYTIKA TONOS 136 //addCombining('\u0344', ACCENT_DIALYTIKA_TONOS); 137 // U+0345: COMBINING GREEK YPOGEGRAMMENI 138 //addCombining('\u0345', ACCENT_YPOGEGRAMMENI); 139 140 // One-way mappings to equivalent preferred accents. 141 // U+0340: COMBINING GRAVE TONE MARK 142 sCombiningToAccent.append('\u0340', ACCENT_GRAVE); 143 // U+0341: COMBINING ACUTE TONE MARK 144 sCombiningToAccent.append('\u0341', ACCENT_ACUTE); 145 // U+0343: COMBINING GREEK KORONIS 146 sCombiningToAccent.append('\u0343', ACCENT_COMMA_ABOVE); 147 148 // One-way legacy mappings to preserve compatibility with older applications. 149 // U+0300: COMBINING GRAVE ACCENT 150 sAccentToCombining.append(ACCENT_GRAVE_LEGACY, '\u0300'); 151 // U+0302: COMBINING CIRCUMFLEX ACCENT 152 sAccentToCombining.append(ACCENT_CIRCUMFLEX_LEGACY, '\u0302'); 153 // U+0303: COMBINING TILDE 154 sAccentToCombining.append(ACCENT_TILDE_LEGACY, '\u0303'); 155 } 156 157 private static void addCombining(int combining, int accent) { 158 sCombiningToAccent.append(combining, accent); 159 sAccentToCombining.append(accent, combining); 160 } 161 162 // Caution! This may only contain chars, not supplementary code points. It's unlikely 163 // it will ever need to, but if it does we'll have to change this 164 private static final SparseIntArray sNonstandardDeadCombinations = new SparseIntArray(); 165 static { 166 // Non-standard decompositions. 167 // Stroke modifier for Finnish multilingual keyboard and others. 168 // U+0110: LATIN CAPITAL LETTER D WITH STROKE 169 addNonStandardDeadCombination(ACCENT_STROKE, 'D', '\u0110'); 170 // U+01E4: LATIN CAPITAL LETTER G WITH STROKE 171 addNonStandardDeadCombination(ACCENT_STROKE, 'G', '\u01e4'); 172 // U+0126: LATIN CAPITAL LETTER H WITH STROKE 173 addNonStandardDeadCombination(ACCENT_STROKE, 'H', '\u0126'); 174 // U+0197: LATIN CAPITAL LETTER I WITH STROKE 175 addNonStandardDeadCombination(ACCENT_STROKE, 'I', '\u0197'); 176 // U+0141: LATIN CAPITAL LETTER L WITH STROKE 177 addNonStandardDeadCombination(ACCENT_STROKE, 'L', '\u0141'); 178 // U+00D8: LATIN CAPITAL LETTER O WITH STROKE 179 addNonStandardDeadCombination(ACCENT_STROKE, 'O', '\u00d8'); 180 // U+0166: LATIN CAPITAL LETTER T WITH STROKE 181 addNonStandardDeadCombination(ACCENT_STROKE, 'T', '\u0166'); 182 // U+0111: LATIN SMALL LETTER D WITH STROKE 183 addNonStandardDeadCombination(ACCENT_STROKE, 'd', '\u0111'); 184 // U+01E5: LATIN SMALL LETTER G WITH STROKE 185 addNonStandardDeadCombination(ACCENT_STROKE, 'g', '\u01e5'); 186 // U+0127: LATIN SMALL LETTER H WITH STROKE 187 addNonStandardDeadCombination(ACCENT_STROKE, 'h', '\u0127'); 188 // U+0268: LATIN SMALL LETTER I WITH STROKE 189 addNonStandardDeadCombination(ACCENT_STROKE, 'i', '\u0268'); 190 // U+0142: LATIN SMALL LETTER L WITH STROKE 191 addNonStandardDeadCombination(ACCENT_STROKE, 'l', '\u0142'); 192 // U+00F8: LATIN SMALL LETTER O WITH STROKE 193 addNonStandardDeadCombination(ACCENT_STROKE, 'o', '\u00f8'); 194 // U+0167: LATIN SMALL LETTER T WITH STROKE 195 addNonStandardDeadCombination(ACCENT_STROKE, 't', '\u0167'); 196 } 197 198 private static void addNonStandardDeadCombination(final int deadCodePoint, 199 final int spacingCodePoint, final int result) { 200 final int combination = (deadCodePoint << 16) | spacingCodePoint; 201 sNonstandardDeadCombinations.put(combination, result); 202 } 203 204 public static final int NOT_A_CHAR = 0; 205 public static final int BITS_TO_SHIFT_DEAD_CODE_POINT_FOR_NON_STANDARD_COMBINATION = 16; 206 // Get a non-standard combination 207 public static char getNonstandardCombination(final int deadCodePoint, 208 final int spacingCodePoint) { 209 final int combination = spacingCodePoint | 210 (deadCodePoint << BITS_TO_SHIFT_DEAD_CODE_POINT_FOR_NON_STANDARD_COMBINATION); 211 return (char)sNonstandardDeadCombinations.get(combination, NOT_A_CHAR); 212 } 213 } 214 215 // TODO: make this a list of events instead 216 final StringBuilder mDeadSequence = new StringBuilder(); 217 218 @Nonnull 219 private static Event createEventChainFromSequence(final @Nonnull CharSequence text, 220 @Nonnull final Event originalEvent) { 221 int index = text.length(); 222 if (index <= 0) { 223 return originalEvent; 224 } 225 Event lastEvent = null; 226 do { 227 final int codePoint = Character.codePointBefore(text, index); 228 lastEvent = Event.createHardwareKeypressEvent(codePoint, 229 originalEvent.mKeyCode, lastEvent, false /* isKeyRepeat */); 230 index -= Character.charCount(codePoint); 231 } while (index > 0); 232 return lastEvent; 233 } 234 235 @Override 236 @Nonnull 237 public Event processEvent(final ArrayList<Event> previousEvents, final Event event) { 238 if (TextUtils.isEmpty(mDeadSequence)) { 239 // No dead char is currently being tracked: this is the most common case. 240 if (event.isDead()) { 241 // The event was a dead key. Start tracking it. 242 mDeadSequence.appendCodePoint(event.mCodePoint); 243 return Event.createConsumedEvent(event); 244 } 245 // Regular keystroke when not keeping track of a dead key. Simply said, there are 246 // no dead keys at all in the current input, so this combiner has nothing to do and 247 // simply returns the event as is. The majority of events will go through this path. 248 return event; 249 } 250 if (Character.isWhitespace(event.mCodePoint) 251 || event.mCodePoint == mDeadSequence.codePointBefore(mDeadSequence.length())) { 252 // When whitespace or twice the same dead key, we should output the dead sequence as is. 253 final Event resultEvent = createEventChainFromSequence(mDeadSequence.toString(), 254 event); 255 mDeadSequence.setLength(0); 256 return resultEvent; 257 } 258 if (event.isFunctionalKeyEvent()) { 259 if (Constants.CODE_DELETE == event.mKeyCode) { 260 // Remove the last code point 261 final int trimIndex = mDeadSequence.length() - Character.charCount( 262 mDeadSequence.codePointBefore(mDeadSequence.length())); 263 mDeadSequence.setLength(trimIndex); 264 return Event.createConsumedEvent(event); 265 } 266 return event; 267 } 268 if (event.isDead()) { 269 mDeadSequence.appendCodePoint(event.mCodePoint); 270 return Event.createConsumedEvent(event); 271 } 272 // Combine normally. 273 final StringBuilder sb = new StringBuilder(); 274 sb.appendCodePoint(event.mCodePoint); 275 int codePointIndex = 0; 276 while (codePointIndex < mDeadSequence.length()) { 277 final int deadCodePoint = mDeadSequence.codePointAt(codePointIndex); 278 final char replacementSpacingChar = 279 Data.getNonstandardCombination(deadCodePoint, event.mCodePoint); 280 if (Data.NOT_A_CHAR != replacementSpacingChar) { 281 sb.setCharAt(0, replacementSpacingChar); 282 } else { 283 final int combining = Data.sAccentToCombining.get(deadCodePoint); 284 sb.appendCodePoint(0 == combining ? deadCodePoint : combining); 285 } 286 codePointIndex += Character.isSupplementaryCodePoint(deadCodePoint) ? 2 : 1; 287 } 288 final String normalizedString = Normalizer.normalize(sb, Normalizer.Form.NFC); 289 final Event resultEvent = createEventChainFromSequence(normalizedString, event); 290 mDeadSequence.setLength(0); 291 return resultEvent; 292 } 293 294 @Override 295 public void reset() { 296 mDeadSequence.setLength(0); 297 } 298 299 @Override 300 public CharSequence getCombiningStateFeedback() { 301 return mDeadSequence; 302 } 303 } 304