067d8cdf56
Change-Id: I538288054a58eb2c81ce3cbe5c9bef900fb653a5
303 lines
14 KiB
Java
303 lines
14 KiB
Java
/*
|
|
* 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<Event> 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;
|
|
}
|
|
}
|