/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.inputmethod.event; import android.text.TextUtils; import android.util.SparseIntArray; import com.android.inputmethod.latin.common.Constants; import java.text.Normalizer; import java.util.ArrayList; import javax.annotation.Nonnull; /** * A combiner that handles dead keys. */ public class DeadKeyCombiner implements Combiner { private static class Data { // This class data taken from KeyCharacterMap.java. /* Characters used to display placeholders for dead keys. */ private static final int ACCENT_ACUTE = '\u00B4'; private static final int ACCENT_BREVE = '\u02D8'; private static final int ACCENT_CARON = '\u02C7'; private static final int ACCENT_CEDILLA = '\u00B8'; private static final int ACCENT_CIRCUMFLEX = '\u02C6'; private static final int ACCENT_COMMA_ABOVE = '\u1FBD'; private static final int ACCENT_COMMA_ABOVE_RIGHT = '\u02BC'; private static final int ACCENT_DOT_ABOVE = '\u02D9'; private static final int ACCENT_DOT_BELOW = Constants.CODE_PERIOD; // approximate private static final int ACCENT_DOUBLE_ACUTE = '\u02DD'; private static final int ACCENT_GRAVE = '\u02CB'; private static final int ACCENT_HOOK_ABOVE = '\u02C0'; private static final int ACCENT_HORN = Constants.CODE_SINGLE_QUOTE; // approximate private static final int ACCENT_MACRON = '\u00AF'; private static final int ACCENT_MACRON_BELOW = '\u02CD'; private static final int ACCENT_OGONEK = '\u02DB'; private static final int ACCENT_REVERSED_COMMA_ABOVE = '\u02BD'; private static final int ACCENT_RING_ABOVE = '\u02DA'; private static final int ACCENT_STROKE = Constants.CODE_DASH; // approximate private static final int ACCENT_TILDE = '\u02DC'; private static final int ACCENT_TURNED_COMMA_ABOVE = '\u02BB'; private static final int ACCENT_UMLAUT = '\u00A8'; private static final int ACCENT_VERTICAL_LINE_ABOVE = '\u02C8'; private static final int ACCENT_VERTICAL_LINE_BELOW = '\u02CC'; /* Legacy dead key display characters used in previous versions of the API (before L) * We still support these characters by mapping them to their non-legacy version. */ private static final int ACCENT_GRAVE_LEGACY = Constants.CODE_GRAVE_ACCENT; private static final int ACCENT_CIRCUMFLEX_LEGACY = Constants.CODE_CIRCUMFLEX_ACCENT; private static final int ACCENT_TILDE_LEGACY = Constants.CODE_TILDE; /** * Maps Unicode combining diacritical to display-form dead key. */ static final SparseIntArray sCombiningToAccent = new SparseIntArray(); static final SparseIntArray sAccentToCombining = new SparseIntArray(); static { // U+0300: COMBINING GRAVE ACCENT addCombining('\u0300', ACCENT_GRAVE); // U+0301: COMBINING ACUTE ACCENT addCombining('\u0301', ACCENT_ACUTE); // U+0302: COMBINING CIRCUMFLEX ACCENT addCombining('\u0302', ACCENT_CIRCUMFLEX); // U+0303: COMBINING TILDE addCombining('\u0303', ACCENT_TILDE); // U+0304: COMBINING MACRON addCombining('\u0304', ACCENT_MACRON); // U+0306: COMBINING BREVE addCombining('\u0306', ACCENT_BREVE); // U+0307: COMBINING DOT ABOVE addCombining('\u0307', ACCENT_DOT_ABOVE); // U+0308: COMBINING DIAERESIS addCombining('\u0308', ACCENT_UMLAUT); // U+0309: COMBINING HOOK ABOVE addCombining('\u0309', ACCENT_HOOK_ABOVE); // U+030A: COMBINING RING ABOVE addCombining('\u030A', ACCENT_RING_ABOVE); // U+030B: COMBINING DOUBLE ACUTE ACCENT addCombining('\u030B', ACCENT_DOUBLE_ACUTE); // U+030C: COMBINING CARON addCombining('\u030C', ACCENT_CARON); // U+030D: COMBINING VERTICAL LINE ABOVE addCombining('\u030D', ACCENT_VERTICAL_LINE_ABOVE); // U+030E: COMBINING DOUBLE VERTICAL LINE ABOVE //addCombining('\u030E', ACCENT_DOUBLE_VERTICAL_LINE_ABOVE); // U+030F: COMBINING DOUBLE GRAVE ACCENT //addCombining('\u030F', ACCENT_DOUBLE_GRAVE); // U+0310: COMBINING CANDRABINDU //addCombining('\u0310', ACCENT_CANDRABINDU); // U+0311: COMBINING INVERTED BREVE //addCombining('\u0311', ACCENT_INVERTED_BREVE); // U+0312: COMBINING TURNED COMMA ABOVE addCombining('\u0312', ACCENT_TURNED_COMMA_ABOVE); // U+0313: COMBINING COMMA ABOVE addCombining('\u0313', ACCENT_COMMA_ABOVE); // U+0314: COMBINING REVERSED COMMA ABOVE addCombining('\u0314', ACCENT_REVERSED_COMMA_ABOVE); // U+0315: COMBINING COMMA ABOVE RIGHT addCombining('\u0315', ACCENT_COMMA_ABOVE_RIGHT); // U+031B: COMBINING HORN addCombining('\u031B', ACCENT_HORN); // U+0323: COMBINING DOT BELOW addCombining('\u0323', ACCENT_DOT_BELOW); // U+0326: COMBINING COMMA BELOW //addCombining('\u0326', ACCENT_COMMA_BELOW); // U+0327: COMBINING CEDILLA addCombining('\u0327', ACCENT_CEDILLA); // U+0328: COMBINING OGONEK addCombining('\u0328', ACCENT_OGONEK); // U+0329: COMBINING VERTICAL LINE BELOW addCombining('\u0329', ACCENT_VERTICAL_LINE_BELOW); // U+0331: COMBINING MACRON BELOW addCombining('\u0331', ACCENT_MACRON_BELOW); // U+0335: COMBINING SHORT STROKE OVERLAY addCombining('\u0335', ACCENT_STROKE); // U+0342: COMBINING GREEK PERISPOMENI //addCombining('\u0342', ACCENT_PERISPOMENI); // U+0344: COMBINING GREEK DIALYTIKA TONOS //addCombining('\u0344', ACCENT_DIALYTIKA_TONOS); // U+0345: COMBINING GREEK YPOGEGRAMMENI //addCombining('\u0345', ACCENT_YPOGEGRAMMENI); // One-way mappings to equivalent preferred accents. // U+0340: COMBINING GRAVE TONE MARK sCombiningToAccent.append('\u0340', ACCENT_GRAVE); // U+0341: COMBINING ACUTE TONE MARK sCombiningToAccent.append('\u0341', ACCENT_ACUTE); // U+0343: COMBINING GREEK KORONIS sCombiningToAccent.append('\u0343', ACCENT_COMMA_ABOVE); // One-way legacy mappings to preserve compatibility with older applications. // U+0300: COMBINING GRAVE ACCENT sAccentToCombining.append(ACCENT_GRAVE_LEGACY, '\u0300'); // U+0302: COMBINING CIRCUMFLEX ACCENT sAccentToCombining.append(ACCENT_CIRCUMFLEX_LEGACY, '\u0302'); // U+0303: COMBINING TILDE sAccentToCombining.append(ACCENT_TILDE_LEGACY, '\u0303'); } private static void addCombining(int combining, int accent) { sCombiningToAccent.append(combining, accent); sAccentToCombining.append(accent, combining); } // Caution! This may only contain chars, not supplementary code points. It's unlikely // it will ever need to, but if it does we'll have to change this private static final SparseIntArray sNonstandardDeadCombinations = new SparseIntArray(); static { // Non-standard decompositions. // Stroke modifier for Finnish multilingual keyboard and others. // U+0110: LATIN CAPITAL LETTER D WITH STROKE addNonStandardDeadCombination(ACCENT_STROKE, 'D', '\u0110'); // U+01E4: LATIN CAPITAL LETTER G WITH STROKE addNonStandardDeadCombination(ACCENT_STROKE, 'G', '\u01e4'); // U+0126: LATIN CAPITAL LETTER H WITH STROKE addNonStandardDeadCombination(ACCENT_STROKE, 'H', '\u0126'); // U+0197: LATIN CAPITAL LETTER I WITH STROKE addNonStandardDeadCombination(ACCENT_STROKE, 'I', '\u0197'); // U+0141: LATIN CAPITAL LETTER L WITH STROKE addNonStandardDeadCombination(ACCENT_STROKE, 'L', '\u0141'); // U+00D8: LATIN CAPITAL LETTER O WITH STROKE addNonStandardDeadCombination(ACCENT_STROKE, 'O', '\u00d8'); // U+0166: LATIN CAPITAL LETTER T WITH STROKE addNonStandardDeadCombination(ACCENT_STROKE, 'T', '\u0166'); // U+0111: LATIN SMALL LETTER D WITH STROKE addNonStandardDeadCombination(ACCENT_STROKE, 'd', '\u0111'); // U+01E5: LATIN SMALL LETTER G WITH STROKE addNonStandardDeadCombination(ACCENT_STROKE, 'g', '\u01e5'); // U+0127: LATIN SMALL LETTER H WITH STROKE addNonStandardDeadCombination(ACCENT_STROKE, 'h', '\u0127'); // U+0268: LATIN SMALL LETTER I WITH STROKE addNonStandardDeadCombination(ACCENT_STROKE, 'i', '\u0268'); // U+0142: LATIN SMALL LETTER L WITH STROKE addNonStandardDeadCombination(ACCENT_STROKE, 'l', '\u0142'); // U+00F8: LATIN SMALL LETTER O WITH STROKE addNonStandardDeadCombination(ACCENT_STROKE, 'o', '\u00f8'); // U+0167: LATIN SMALL LETTER T WITH STROKE addNonStandardDeadCombination(ACCENT_STROKE, 't', '\u0167'); } private static void addNonStandardDeadCombination(final int deadCodePoint, final int spacingCodePoint, final int result) { final int combination = (deadCodePoint << 16) | spacingCodePoint; sNonstandardDeadCombinations.put(combination, result); } public static final int NOT_A_CHAR = 0; public static final int BITS_TO_SHIFT_DEAD_CODE_POINT_FOR_NON_STANDARD_COMBINATION = 16; // Get a non-standard combination public static char getNonstandardCombination(final int deadCodePoint, final int spacingCodePoint) { final int combination = spacingCodePoint | (deadCodePoint << BITS_TO_SHIFT_DEAD_CODE_POINT_FOR_NON_STANDARD_COMBINATION); return (char)sNonstandardDeadCombinations.get(combination, NOT_A_CHAR); } } // TODO: make this a list of events instead final StringBuilder mDeadSequence = new StringBuilder(); @Nonnull private static Event createEventChainFromSequence(final @Nonnull CharSequence text, @Nonnull final Event originalEvent) { int index = text.length(); if (index <= 0) { return originalEvent; } Event lastEvent = null; do { final int codePoint = Character.codePointBefore(text, index); lastEvent = Event.createHardwareKeypressEvent(codePoint, originalEvent.mKeyCode, lastEvent, false /* isKeyRepeat */); index -= Character.charCount(codePoint); } while (index > 0); return lastEvent; } @Override @Nonnull public Event processEvent(final ArrayList previousEvents, final Event event) { if (TextUtils.isEmpty(mDeadSequence)) { // No dead char is currently being tracked: this is the most common case. if (event.isDead()) { // The event was a dead key. Start tracking it. mDeadSequence.appendCodePoint(event.mCodePoint); return Event.createConsumedEvent(event); } // Regular keystroke when not keeping track of a dead key. Simply said, there are // no dead keys at all in the current input, so this combiner has nothing to do and // simply returns the event as is. The majority of events will go through this path. return event; } if (Character.isWhitespace(event.mCodePoint) || event.mCodePoint == mDeadSequence.codePointBefore(mDeadSequence.length())) { // When whitespace or twice the same dead key, we should output the dead sequence as is. final Event resultEvent = createEventChainFromSequence(mDeadSequence.toString(), event); mDeadSequence.setLength(0); return resultEvent; } if (event.isFunctionalKeyEvent()) { if (Constants.CODE_DELETE == event.mKeyCode) { // Remove the last code point final int trimIndex = mDeadSequence.length() - Character.charCount( mDeadSequence.codePointBefore(mDeadSequence.length())); mDeadSequence.setLength(trimIndex); return Event.createConsumedEvent(event); } return event; } if (event.isDead()) { mDeadSequence.appendCodePoint(event.mCodePoint); return Event.createConsumedEvent(event); } // Combine normally. final StringBuilder sb = new StringBuilder(); sb.appendCodePoint(event.mCodePoint); int codePointIndex = 0; while (codePointIndex < mDeadSequence.length()) { final int deadCodePoint = mDeadSequence.codePointAt(codePointIndex); final char replacementSpacingChar = Data.getNonstandardCombination(deadCodePoint, event.mCodePoint); if (Data.NOT_A_CHAR != replacementSpacingChar) { sb.setCharAt(0, replacementSpacingChar); } else { final int combining = Data.sAccentToCombining.get(deadCodePoint); sb.appendCodePoint(0 == combining ? deadCodePoint : combining); } codePointIndex += Character.isSupplementaryCodePoint(deadCodePoint) ? 2 : 1; } final String normalizedString = Normalizer.normalize(sb, Normalizer.Form.NFC); final Event resultEvent = createEventChainFromSequence(normalizedString, event); mDeadSequence.setLength(0); return resultEvent; } @Override public void reset() { mDeadSequence.setLength(0); } @Override public CharSequence getCombiningStateFeedback() { return mDeadSequence; } }