diff options
Diffstat (limited to 'java/src/org/kelar/inputmethod/event')
8 files changed, 1057 insertions, 0 deletions
diff --git a/java/src/org/kelar/inputmethod/event/Combiner.java b/java/src/org/kelar/inputmethod/event/Combiner.java new file mode 100644 index 000000000..45945d460 --- /dev/null +++ b/java/src/org/kelar/inputmethod/event/Combiner.java @@ -0,0 +1,51 @@ +/* + * 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 org.kelar.inputmethod.event; + +import java.util.ArrayList; + +import javax.annotation.Nonnull; + +/** + * A generic interface for combiners. Combiners are objects that transform chains of input events + * into committable strings and manage feedback to show to the user on the combining state. + */ +public interface Combiner { + /** + * Process an event, possibly combining it with the existing state and return the new event. + * + * If this event does not result in any new event getting passed down the chain, this method + * returns null. It may also modify the previous event list if appropriate. + * + * @param previousEvents the previous events in this composition. + * @param event the event to combine with the existing state. + * @return the resulting event. + */ + @Nonnull + Event processEvent(ArrayList<Event> previousEvents, Event event); + + /** + * Get the feedback that should be shown to the user for the current state of this combiner. + * @return A CharSequence representing the feedback to show users. It may include styles. + */ + CharSequence getCombiningStateFeedback(); + + /** + * Reset the state of this combiner, for example when the cursor was moved. + */ + void reset(); +} diff --git a/java/src/org/kelar/inputmethod/event/CombinerChain.java b/java/src/org/kelar/inputmethod/event/CombinerChain.java new file mode 100644 index 000000000..afd992e62 --- /dev/null +++ b/java/src/org/kelar/inputmethod/event/CombinerChain.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.event; + +import android.text.SpannableStringBuilder; +import android.text.TextUtils; + +import org.kelar.inputmethod.latin.common.Constants; + +import java.util.ArrayList; + +import javax.annotation.Nonnull; + +/** + * This class implements the logic chain between receiving events and generating code points. + * + * Event sources are multiple. It may be a hardware keyboard, a D-PAD, a software keyboard, + * or any exotic input source. + * This class will orchestrate the composing chain that starts with an event as its input. Each + * composer will be given turns one after the other. + * The output is composed of two sequences of code points: the first, representing the already + * finished combining part, will be shown normally as the composing string, while the second is + * feedback on the composing state and will typically be shown with different styling such as + * a colored background. + */ +public class CombinerChain { + // The already combined text, as described above + private StringBuilder mCombinedText; + // The feedback on the composing state, as described above + private SpannableStringBuilder mStateFeedback; + private final ArrayList<Combiner> mCombiners; + + /** + * Create an combiner chain. + * + * The combiner chain takes events as inputs and outputs code points and combining state. + * For example, if the input language is Japanese, the combining chain will typically perform + * kana conversion. This takes a string for initial text, taken to be present before the + * cursor: we'll start after this. + * + * @param initialText The text that has already been combined so far. + */ + public CombinerChain(final String initialText) { + mCombiners = new ArrayList<>(); + // The dead key combiner is always active, and always first + mCombiners.add(new DeadKeyCombiner()); + mCombinedText = new StringBuilder(initialText); + mStateFeedback = new SpannableStringBuilder(); + } + + public void reset() { + mCombinedText.setLength(0); + mStateFeedback.clear(); + for (final Combiner c : mCombiners) { + c.reset(); + } + } + + private void updateStateFeedback() { + mStateFeedback.clear(); + for (int i = mCombiners.size() - 1; i >= 0; --i) { + mStateFeedback.append(mCombiners.get(i).getCombiningStateFeedback()); + } + } + + /** + * Process an event through the combining chain, and return a processed event to apply. + * @param previousEvents the list of previous events in this composition + * @param newEvent the new event to process + * @return the processed event. It may be the same event, or a consumed event, or a completely + * new event. However it may never be null. + */ + @Nonnull + public Event processEvent(final ArrayList<Event> previousEvents, + @Nonnull final Event newEvent) { + final ArrayList<Event> modifiablePreviousEvents = new ArrayList<>(previousEvents); + Event event = newEvent; + for (final Combiner combiner : mCombiners) { + // A combiner can never return more than one event; it can return several + // code points, but they should be encapsulated within one event. + event = combiner.processEvent(modifiablePreviousEvents, event); + if (event.isConsumed()) { + // If the event is consumed, then we don't pass it to subsequent combiners: + // they should not see it at all. + break; + } + } + updateStateFeedback(); + return event; + } + + /** + * Apply a processed event. + * @param event the event to be applied + */ + public void applyProcessedEvent(final Event event) { + if (null != event) { + // TODO: figure out the generic way of doing this + if (Constants.CODE_DELETE == event.mKeyCode) { + final int length = mCombinedText.length(); + if (length > 0) { + final int lastCodePoint = mCombinedText.codePointBefore(length); + mCombinedText.delete(length - Character.charCount(lastCodePoint), length); + } + } else { + final CharSequence textToCommit = event.getTextToCommit(); + if (!TextUtils.isEmpty(textToCommit)) { + mCombinedText.append(textToCommit); + } + } + } + updateStateFeedback(); + } + + /** + * Get the char sequence that should be displayed as the composing word. It may include + * styling spans. + */ + public CharSequence getComposingWordWithCombiningFeedback() { + final SpannableStringBuilder s = new SpannableStringBuilder(mCombinedText); + return s.append(mStateFeedback); + } +} diff --git a/java/src/org/kelar/inputmethod/event/DeadKeyCombiner.java b/java/src/org/kelar/inputmethod/event/DeadKeyCombiner.java new file mode 100644 index 000000000..562fc2761 --- /dev/null +++ b/java/src/org/kelar/inputmethod/event/DeadKeyCombiner.java @@ -0,0 +1,303 @@ +/* + * 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 org.kelar.inputmethod.event; + +import android.text.TextUtils; +import android.util.SparseIntArray; + +import org.kelar.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; + } +} diff --git a/java/src/org/kelar/inputmethod/event/Event.java b/java/src/org/kelar/inputmethod/event/Event.java new file mode 100644 index 000000000..17c9717c5 --- /dev/null +++ b/java/src/org/kelar/inputmethod/event/Event.java @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.event; + +import org.kelar.inputmethod.annotations.ExternallyReferenced; +import org.kelar.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import org.kelar.inputmethod.latin.common.Constants; +import org.kelar.inputmethod.latin.common.StringUtils; + +import javax.annotation.Nonnull; + +/** + * Class representing a generic input event as handled by Latin IME. + * + * This contains information about the origin of the event, but it is generalized and should + * represent a software keypress, hardware keypress, or d-pad move alike. + * Very importantly, this does not necessarily result in inputting one character, or even anything + * at all - it may be a dead key, it may be a partial input, it may be a special key on the + * keyboard, it may be a cancellation of a keypress (e.g. in a soft keyboard the finger of the + * user has slid out of the key), etc. It may also be a batch input from a gesture or handwriting + * for example. + * The combiner should figure out what to do with this. + */ +public class Event { + // Should the types below be represented by separate classes instead? It would be cleaner + // but probably a bit too much + // An event we don't handle in Latin IME, for example pressing Ctrl on a hardware keyboard. + final public static int EVENT_TYPE_NOT_HANDLED = 0; + // A key press that is part of input, for example pressing an alphabetic character on a + // hardware qwerty keyboard. It may be part of a sequence that will be re-interpreted later + // through combination. + final public static int EVENT_TYPE_INPUT_KEYPRESS = 1; + // A toggle event is triggered by a key that affects the previous character. An example would + // be a numeric key on a 10-key keyboard, which would toggle between 1 - a - b - c with + // repeated presses. + final public static int EVENT_TYPE_TOGGLE = 2; + // A mode event instructs the combiner to change modes. The canonical example would be the + // hankaku/zenkaku key on a Japanese keyboard, or even the caps lock key on a qwerty keyboard + // if handled at the combiner level. + final public static int EVENT_TYPE_MODE_KEY = 3; + // An event corresponding to a gesture. + final public static int EVENT_TYPE_GESTURE = 4; + // An event corresponding to the manual pick of a suggestion. + final public static int EVENT_TYPE_SUGGESTION_PICKED = 5; + // An event corresponding to a string generated by some software process. + final public static int EVENT_TYPE_SOFTWARE_GENERATED_STRING = 6; + // An event corresponding to a cursor move + final public static int EVENT_TYPE_CURSOR_MOVE = 7; + + // 0 is a valid code point, so we use -1 here. + final public static int NOT_A_CODE_POINT = -1; + // -1 is a valid key code, so we use 0 here. + final public static int NOT_A_KEY_CODE = 0; + + final private static int FLAG_NONE = 0; + // This event is a dead character, usually input by a dead key. Examples include dead-acute + // or dead-abovering. + final private static int FLAG_DEAD = 0x1; + // This event is coming from a key repeat, software or hardware. + final private static int FLAG_REPEAT = 0x2; + // This event has already been consumed. + final private static int FLAG_CONSUMED = 0x4; + + final private int mEventType; // The type of event - one of the constants above + // The code point associated with the event, if relevant. This is a unicode code point, and + // has nothing to do with other representations of the key. It is only relevant if this event + // is of KEYPRESS type, but for a mode key like hankaku/zenkaku or ctrl, there is no code point + // associated so this should be NOT_A_CODE_POINT to avoid unintentional use of its value when + // it's not relevant. + final public int mCodePoint; + + // If applicable, this contains the string that should be input. + final public CharSequence mText; + + // The key code associated with the event, if relevant. This is relevant whenever this event + // has been triggered by a key press, but not for a gesture for example. This has conceptually + // no link to the code point, although keys that enter a straight code point may often set + // this to be equal to mCodePoint for convenience. If this is not a key, this must contain + // NOT_A_KEY_CODE. + final public int mKeyCode; + + // Coordinates of the touch event, if relevant. If useful, we may want to replace this with + // a MotionEvent or something in the future. This is only relevant when the keypress is from + // a software keyboard obviously, unless there are touch-sensitive hardware keyboards in the + // future or some other awesome sauce. + final public int mX; + final public int mY; + + // Some flags that can't go into the key code. It's a bit field of FLAG_* + final private int mFlags; + + // If this is of type EVENT_TYPE_SUGGESTION_PICKED, this must not be null (and must be null in + // other cases). + final public SuggestedWordInfo mSuggestedWordInfo; + + // The next event, if any. Null if there is no next event yet. + final public Event mNextEvent; + + // This method is private - to create a new event, use one of the create* utility methods. + private Event(final int type, final CharSequence text, final int codePoint, final int keyCode, + final int x, final int y, final SuggestedWordInfo suggestedWordInfo, final int flags, + final Event next) { + mEventType = type; + mText = text; + mCodePoint = codePoint; + mKeyCode = keyCode; + mX = x; + mY = y; + mSuggestedWordInfo = suggestedWordInfo; + mFlags = flags; + mNextEvent = next; + // Validity checks + // mSuggestedWordInfo is non-null if and only if the type is SUGGESTION_PICKED + if (EVENT_TYPE_SUGGESTION_PICKED == mEventType) { + if (null == mSuggestedWordInfo) { + throw new RuntimeException("Wrong event: SUGGESTION_PICKED event must have a " + + "non-null SuggestedWordInfo"); + } + } else { + if (null != mSuggestedWordInfo) { + throw new RuntimeException("Wrong event: only SUGGESTION_PICKED events may have " + + "a non-null SuggestedWordInfo"); + } + } + } + + @Nonnull + public static Event createSoftwareKeypressEvent(final int codePoint, final int keyCode, + final int x, final int y, final boolean isKeyRepeat) { + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, keyCode, x, y, + null /* suggestedWordInfo */, isKeyRepeat ? FLAG_REPEAT : FLAG_NONE, null); + } + + @Nonnull + public static Event createHardwareKeypressEvent(final int codePoint, final int keyCode, + final Event next, final boolean isKeyRepeat) { + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, keyCode, + Constants.EXTERNAL_KEYBOARD_COORDINATE, Constants.EXTERNAL_KEYBOARD_COORDINATE, + null /* suggestedWordInfo */, isKeyRepeat ? FLAG_REPEAT : FLAG_NONE, next); + } + + // This creates an input event for a dead character. @see {@link #FLAG_DEAD} + @ExternallyReferenced + @Nonnull + public static Event createDeadEvent(final int codePoint, final int keyCode, final Event next) { + // TODO: add an argument or something if we ever create a software layout with dead keys. + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, keyCode, + Constants.EXTERNAL_KEYBOARD_COORDINATE, Constants.EXTERNAL_KEYBOARD_COORDINATE, + null /* suggestedWordInfo */, FLAG_DEAD, next); + } + + /** + * Create an input event with nothing but a code point. This is the most basic possible input + * event; it contains no information on many things the IME requires to function correctly, + * so avoid using it unless really nothing is known about this input. + * @param codePoint the code point. + * @return an event for this code point. + */ + @Nonnull + public static Event createEventForCodePointFromUnknownSource(final int codePoint) { + // TODO: should we have a different type of event for this? After all, it's not a key press. + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, NOT_A_KEY_CODE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + null /* suggestedWordInfo */, FLAG_NONE, null /* next */); + } + + /** + * Creates an input event with a code point and x, y coordinates. This is typically used when + * resuming a previously-typed word, when the coordinates are still known. + * @param codePoint the code point to input. + * @param x the X coordinate. + * @param y the Y coordinate. + * @return an event for this code point and coordinates. + */ + @Nonnull + public static Event createEventForCodePointFromAlreadyTypedText(final int codePoint, + final int x, final int y) { + // TODO: should we have a different type of event for this? After all, it's not a key press. + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, NOT_A_KEY_CODE, + x, y, null /* suggestedWordInfo */, FLAG_NONE, null /* next */); + } + + /** + * Creates an input event representing the manual pick of a suggestion. + * @return an event for this suggestion pick. + */ + @Nonnull + public static Event createSuggestionPickedEvent(final SuggestedWordInfo suggestedWordInfo) { + return new Event(EVENT_TYPE_SUGGESTION_PICKED, suggestedWordInfo.mWord, + NOT_A_CODE_POINT, NOT_A_KEY_CODE, + Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, + suggestedWordInfo, FLAG_NONE, null /* next */); + } + + /** + * Creates an input event with a CharSequence. This is used by some software processes whose + * output is a string, possibly with styling. Examples include press on a multi-character key, + * or combination that outputs a string. + * @param text the CharSequence associated with this event. + * @param keyCode the key code, or NOT_A_KEYCODE if not applicable. + * @return an event for this text. + */ + @Nonnull + public static Event createSoftwareTextEvent(final CharSequence text, final int keyCode) { + return new Event(EVENT_TYPE_SOFTWARE_GENERATED_STRING, text, NOT_A_CODE_POINT, keyCode, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + null /* suggestedWordInfo */, FLAG_NONE, null /* next */); + } + + /** + * Creates an input event representing the manual pick of a punctuation suggestion. + * @return an event for this suggestion pick. + */ + @Nonnull + public static Event createPunctuationSuggestionPickedEvent( + final SuggestedWordInfo suggestedWordInfo) { + final int primaryCode = suggestedWordInfo.mWord.charAt(0); + return new Event(EVENT_TYPE_SUGGESTION_PICKED, suggestedWordInfo.mWord, primaryCode, + NOT_A_KEY_CODE, Constants.SUGGESTION_STRIP_COORDINATE, + Constants.SUGGESTION_STRIP_COORDINATE, suggestedWordInfo, FLAG_NONE, + null /* next */); + } + + /** + * Creates an input event representing moving the cursor. The relative move amount is stored + * in mX. + * @param moveAmount the relative move amount. + * @return an event for this cursor move. + */ + @Nonnull + public static Event createCursorMovedEvent(final int moveAmount) { + return new Event(EVENT_TYPE_CURSOR_MOVE, null, NOT_A_CODE_POINT, NOT_A_KEY_CODE, + moveAmount, Constants.NOT_A_COORDINATE, null, FLAG_NONE, null); + } + + /** + * Creates an event identical to the passed event, but that has already been consumed. + * @param source the event to copy the properties of. + * @return an identical event marked as consumed. + */ + @Nonnull + public static Event createConsumedEvent(final Event source) { + // A consumed event should not input any text at all, so we pass the empty string as text. + return new Event(source.mEventType, source.mText, source.mCodePoint, source.mKeyCode, + source.mX, source.mY, source.mSuggestedWordInfo, source.mFlags | FLAG_CONSUMED, + source.mNextEvent); + } + + @Nonnull + public static Event createNotHandledEvent() { + return new Event(EVENT_TYPE_NOT_HANDLED, null /* text */, NOT_A_CODE_POINT, NOT_A_KEY_CODE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + null /* suggestedWordInfo */, FLAG_NONE, null); + } + + // Returns whether this is a function key like backspace, ctrl, settings... as opposed to keys + // that result in input like letters or space. + public boolean isFunctionalKeyEvent() { + // This logic may need to be refined in the future + return NOT_A_CODE_POINT == mCodePoint; + } + + // Returns whether this event is for a dead character. @see {@link #FLAG_DEAD} + public boolean isDead() { + return 0 != (FLAG_DEAD & mFlags); + } + + public boolean isKeyRepeat() { + return 0 != (FLAG_REPEAT & mFlags); + } + + public boolean isConsumed() { return 0 != (FLAG_CONSUMED & mFlags); } + + public boolean isGesture() { return EVENT_TYPE_GESTURE == mEventType; } + + // Returns whether this is a fake key press from the suggestion strip. This happens with + // punctuation signs selected from the suggestion strip. + public boolean isSuggestionStripPress() { + return EVENT_TYPE_SUGGESTION_PICKED == mEventType; + } + + public boolean isHandled() { + return EVENT_TYPE_NOT_HANDLED != mEventType; + } + + public CharSequence getTextToCommit() { + if (isConsumed()) { + return ""; // A consumed event should input no text. + } + switch (mEventType) { + case EVENT_TYPE_MODE_KEY: + case EVENT_TYPE_NOT_HANDLED: + case EVENT_TYPE_TOGGLE: + case EVENT_TYPE_CURSOR_MOVE: + return ""; + case EVENT_TYPE_INPUT_KEYPRESS: + return StringUtils.newSingleCodePointString(mCodePoint); + case EVENT_TYPE_GESTURE: + case EVENT_TYPE_SOFTWARE_GENERATED_STRING: + case EVENT_TYPE_SUGGESTION_PICKED: + return mText; + } + throw new RuntimeException("Unknown event type: " + mEventType); + } +} diff --git a/java/src/org/kelar/inputmethod/event/EventDecoder.java b/java/src/org/kelar/inputmethod/event/EventDecoder.java new file mode 100644 index 000000000..3826f0608 --- /dev/null +++ b/java/src/org/kelar/inputmethod/event/EventDecoder.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.event; + +/** + * A generic interface for event decoders. + */ +public interface EventDecoder { + +} diff --git a/java/src/org/kelar/inputmethod/event/HardwareEventDecoder.java b/java/src/org/kelar/inputmethod/event/HardwareEventDecoder.java new file mode 100644 index 000000000..0b75ab8b3 --- /dev/null +++ b/java/src/org/kelar/inputmethod/event/HardwareEventDecoder.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.event; + +import android.view.KeyEvent; + +/** + * An event decoder for hardware events. + */ +public interface HardwareEventDecoder extends EventDecoder { + public Event decodeHardwareKey(final KeyEvent keyEvent); +} diff --git a/java/src/org/kelar/inputmethod/event/HardwareKeyboardEventDecoder.java b/java/src/org/kelar/inputmethod/event/HardwareKeyboardEventDecoder.java new file mode 100644 index 000000000..c8cfbc2cc --- /dev/null +++ b/java/src/org/kelar/inputmethod/event/HardwareKeyboardEventDecoder.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.event; + +import android.view.KeyCharacterMap; +import android.view.KeyEvent; + +import org.kelar.inputmethod.latin.common.Constants; + +/** + * A hardware event decoder for a hardware qwerty-ish keyboard. + * + * The events are always hardware keypresses, but they can be key down or key up events, they + * can be dead keys, they can be meta keys like shift or ctrl... This does not deal with + * 10-key like keyboards; a different decoder is used for this. + */ +public class HardwareKeyboardEventDecoder implements HardwareEventDecoder { + final int mDeviceId; + + public HardwareKeyboardEventDecoder(final int deviceId) { + mDeviceId = deviceId; + // TODO: get the layout for this hardware keyboard + } + + @Override + public Event decodeHardwareKey(final KeyEvent keyEvent) { + // KeyEvent#getUnicodeChar() does not exactly returns a unicode char, but rather a value + // that includes both the unicode char in the lower 21 bits and flags in the upper bits, + // hence the name "codePointAndFlags". {@see KeyEvent#getUnicodeChar()} for more info. + final int codePointAndFlags = keyEvent.getUnicodeChar(); + // The keyCode is the abstraction used by the KeyEvent to represent different keys that + // do not necessarily map to a unicode character. This represents a physical key, like + // the key for 'A' or Space, but also Backspace or Ctrl or Caps Lock. + final int keyCode = keyEvent.getKeyCode(); + final boolean isKeyRepeat = (0 != keyEvent.getRepeatCount()); + if (KeyEvent.KEYCODE_DEL == keyCode) { + return Event.createHardwareKeypressEvent(Event.NOT_A_CODE_POINT, Constants.CODE_DELETE, + null /* next */, isKeyRepeat); + } + if (keyEvent.isPrintingKey() || KeyEvent.KEYCODE_SPACE == keyCode + || KeyEvent.KEYCODE_ENTER == keyCode) { + if (0 != (codePointAndFlags & KeyCharacterMap.COMBINING_ACCENT)) { + // A dead key. + return Event.createDeadEvent( + codePointAndFlags & KeyCharacterMap.COMBINING_ACCENT_MASK, keyCode, + null /* next */); + } + if (KeyEvent.KEYCODE_ENTER == keyCode) { + // The Enter key. If the Shift key is not being pressed, this should send a + // CODE_ENTER to trigger the action if any, or a carriage return otherwise. If the + // Shift key is being pressed, this should send a CODE_SHIFT_ENTER and let + // Latin IME decide what to do with it. + if (keyEvent.isShiftPressed()) { + return Event.createHardwareKeypressEvent(Event.NOT_A_CODE_POINT, + Constants.CODE_SHIFT_ENTER, null /* next */, isKeyRepeat); + } + return Event.createHardwareKeypressEvent(Constants.CODE_ENTER, keyCode, + null /* next */, isKeyRepeat); + } + // If not Enter, then this is just a regular keypress event for a normal character + // that can be committed right away, taking into account the current state. + return Event.createHardwareKeypressEvent(codePointAndFlags, keyCode, null /* next */, + isKeyRepeat); + } + return Event.createNotHandledEvent(); + } +} diff --git a/java/src/org/kelar/inputmethod/event/InputTransaction.java b/java/src/org/kelar/inputmethod/event/InputTransaction.java new file mode 100644 index 000000000..096ae68f5 --- /dev/null +++ b/java/src/org/kelar/inputmethod/event/InputTransaction.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.event; + +import org.kelar.inputmethod.latin.settings.SettingsValues; + +/** + * An object encapsulating a single transaction for input. + */ +public class InputTransaction { + // UPDATE_LATER is stronger than UPDATE_NOW. The reason for this is, if we have to update later, + // it's because something will change that we can't evaluate now, which means that even if we + // re-evaluate now we'll have to do it again later. The only case where that wouldn't apply + // would be if we needed to update now to find out the new state right away, but then we + // can't do it with this deferred mechanism anyway. + public static final int SHIFT_NO_UPDATE = 0; + public static final int SHIFT_UPDATE_NOW = 1; + public static final int SHIFT_UPDATE_LATER = 2; + + // Initial conditions + public final SettingsValues mSettingsValues; + public final Event mEvent; + public final long mTimestamp; + public final int mSpaceState; + public final int mShiftState; + + // Outputs + private int mRequiredShiftUpdate = SHIFT_NO_UPDATE; + private boolean mRequiresUpdateSuggestions = false; + private boolean mDidAffectContents = false; + private boolean mDidAutoCorrect = false; + + public InputTransaction(final SettingsValues settingsValues, final Event event, + final long timestamp, final int spaceState, final int shiftState) { + mSettingsValues = settingsValues; + mEvent = event; + mTimestamp = timestamp; + mSpaceState = spaceState; + mShiftState = shiftState; + } + + /** + * Indicate that this transaction requires some type of shift update. + * @param updateType What type of shift update this requires. + */ + public void requireShiftUpdate(final int updateType) { + mRequiredShiftUpdate = Math.max(mRequiredShiftUpdate, updateType); + } + + /** + * Gets what type of shift update this transaction requires. + * @return The shift update type. + */ + public int getRequiredShiftUpdate() { + return mRequiredShiftUpdate; + } + + /** + * Indicate that this transaction requires updating the suggestions. + */ + public void setRequiresUpdateSuggestions() { + mRequiresUpdateSuggestions = true; + } + + /** + * Find out whether this transaction requires updating the suggestions. + * @return Whether this transaction requires updating the suggestions. + */ + public boolean requiresUpdateSuggestions() { + return mRequiresUpdateSuggestions; + } + + /** + * Indicate that this transaction affected the contents of the editor. + */ + public void setDidAffectContents() { + mDidAffectContents = true; + } + + /** + * Find out whether this transaction affected contents of the editor. + * @return Whether this transaction affected contents of the editor. + */ + public boolean didAffectContents() { + return mDidAffectContents; + } + + /** + * Indicate that this transaction performed an auto-correction. + */ + public void setDidAutoCorrect() { + mDidAutoCorrect = true; + } + + /** + * Find out whether this transaction performed an auto-correction. + * @return Whether this transaction performed an auto-correction. + */ + public boolean didAutoCorrect() { + return mDidAutoCorrect; + } +} |