diff options
Diffstat (limited to 'java/src/com/android/inputmethod/event')
11 files changed, 777 insertions, 259 deletions
diff --git a/java/src/com/android/inputmethod/event/Combiner.java b/java/src/com/android/inputmethod/event/Combiner.java index ab6b70c04..8b808c6b3 100644 --- a/java/src/com/android/inputmethod/event/Combiner.java +++ b/java/src/com/android/inputmethod/event/Combiner.java @@ -16,14 +16,33 @@ package com.android.inputmethod.event; +import java.util.ArrayList; + /** - * A generic interface for combiners. + * 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 { /** - * Combine an event with the existing state and return the new event. + * 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. */ - Event combine(Event event); + 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/com/android/inputmethod/event/CombinerChain.java b/java/src/com/android/inputmethod/event/CombinerChain.java new file mode 100644 index 000000000..61bc11b39 --- /dev/null +++ b/java/src/com/android/inputmethod/event/CombinerChain.java @@ -0,0 +1,155 @@ +/* + * 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 com.android.inputmethod.event; + +import android.text.SpannableStringBuilder; +import android.text.TextUtils; + +import com.android.inputmethod.latin.Constants; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * 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; + + private static final HashMap<String, Class<? extends Combiner>> IMPLEMENTED_COMBINERS = + new HashMap<>(); + static { + IMPLEMENTED_COMBINERS.put("MyanmarReordering", MyanmarReordering.class); + } + private static final String COMBINER_SPEC_SEPARATOR = ";"; + + /** + * 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. + * @param combinerList A list of combiners to be applied in order. + */ + public CombinerChain(final String initialText, final Combiner... combinerList) { + mCombiners = new ArrayList<>(); + // The dead key combiner is always active, and always first + mCombiners.add(new DeadKeyCombiner()); + for (final Combiner combiner : combinerList) { + mCombiners.add(combiner); + } + mCombinedText = new StringBuilder(initialText); + mStateFeedback = new SpannableStringBuilder(); + } + + public void reset() { + mCombinedText.setLength(0); + mStateFeedback.clear(); + for (final Combiner c : mCombiners) { + c.reset(); + } + } + + /** + * Pass a new event through the whole chain. + * @param previousEvents the list of previous events in this composition + * @param newEvent the new event to process + */ + public void processEvent(final ArrayList<Event> previousEvents, 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 (null == event) { + // Combiners return null if they eat the event. + break; + } + } + 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); + } + } + } + mStateFeedback.clear(); + for (int i = mCombiners.size() - 1; i >= 0; --i) { + mStateFeedback.append(mCombiners.get(i).getCombiningStateFeedback()); + } + } + + /** + * 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); + } + + public static Combiner[] createCombiners(final String spec) { + if (TextUtils.isEmpty(spec)) { + return new Combiner[0]; + } + final String[] combinerDescriptors = spec.split(COMBINER_SPEC_SEPARATOR); + final Combiner[] combiners = new Combiner[combinerDescriptors.length]; + int i = 0; + for (final String combinerDescriptor : combinerDescriptors) { + final Class<? extends Combiner> combinerClass = + IMPLEMENTED_COMBINERS.get(combinerDescriptor); + if (null == combinerClass) { + throw new RuntimeException("Unknown combiner descriptor: " + combinerDescriptor); + } + try { + combiners[i++] = combinerClass.newInstance(); + } catch (InstantiationException e) { + throw new RuntimeException("Unable to instantiate combiner: " + combinerDescriptor, + e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Unable to instantiate combiner: " + combinerDescriptor, + e); + } + } + return combiners; + } +} diff --git a/java/src/com/android/inputmethod/event/DeadKeyCombiner.java b/java/src/com/android/inputmethod/event/DeadKeyCombiner.java index 52987d571..bef4d8594 100644 --- a/java/src/com/android/inputmethod/event/DeadKeyCombiner.java +++ b/java/src/com/android/inputmethod/event/DeadKeyCombiner.java @@ -21,14 +21,17 @@ import android.view.KeyCharacterMap; import com.android.inputmethod.latin.Constants; +import java.util.ArrayList; + /** * A combiner that handles dead keys. */ public class DeadKeyCombiner implements Combiner { + // TODO: make this a list of events instead final StringBuilder mDeadSequence = new StringBuilder(); @Override - public Event combine(final Event event) { + public Event processEvent(final ArrayList<Event> previousEvents, final Event event) { if (null == event) return null; // Just in case some combiner is broken if (TextUtils.isEmpty(mDeadSequence)) { if (event.isDead()) { @@ -43,19 +46,33 @@ public class DeadKeyCombiner implements Combiner { final int resultingCodePoint = KeyCharacterMap.getDeadChar(deadCodePoint, event.mCodePoint); if (0 == resultingCodePoint) { - // We can't combine both characters. We need to commit the dead key as a committable + // We can't combine both characters. We need to commit the dead key as a separate // character, and the next char too unless it's a space (because as a special case, // dead key + space should result in only the dead key being committed - that's // how dead keys work). // If the event is a space, we should commit the dead char alone, but if it's // not, we need to commit both. - return Event.createCommittableEvent(deadCodePoint, - Constants.CODE_SPACE == event.mCodePoint ? null : event /* next */); + // TODO: this is not necessarily triggered by hardware key events, so it's not + // a good idea to masquerade as one. This should be typed as a software + // composite event or something. + return Event.createHardwareKeypressEvent(deadCodePoint, event.mKeyCode, + Constants.CODE_SPACE == event.mCodePoint ? null : event /* next */, + false /* isKeyRepeat */); } else { // We could combine the characters. - return Event.createCommittableEvent(resultingCodePoint, null /* next */); + return Event.createHardwareKeypressEvent(resultingCodePoint, event.mKeyCode, + null /* next */, false /* isKeyRepeat */); } } } + @Override + public void reset() { + mDeadSequence.setLength(0); + } + + @Override + public CharSequence getCombiningStateFeedback() { + return mDeadSequence; + } } diff --git a/java/src/com/android/inputmethod/event/Event.java b/java/src/com/android/inputmethod/event/Event.java index 1f3320eb7..d257441e0 100644 --- a/java/src/com/android/inputmethod/event/Event.java +++ b/java/src/com/android/inputmethod/event/Event.java @@ -16,6 +16,10 @@ package com.android.inputmethod.event; +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.utils.StringUtils; + /** * Class representing a generic input event as handled by Latin IME. * @@ -32,62 +36,234 @@ 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_NOT_HANDLED = 0; - // A character that is already final, for example pressing an alphabetic character on a - // hardware qwerty keyboard. - final public static int EVENT_COMMITTABLE = 1; - // A dead key, which means a character that should combine with what is coming next. Examples - // include the "^" character on an azerty keyboard which combines with "e" to make "ê", or - // AltGr+' on a dvorak international keyboard which combines with "e" to make "é". This is - // true regardless of the language or combining mode, and should be seen as a property of the - // key - a dead key followed by another key with which it can combine should be regarded as if - // the keyboard actually had such a key. - final public static int EVENT_DEAD = 2; + 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_TOGGLE = 3; + 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_MODE_KEY = 4; + 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; + + // 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 NOT_A_CODE_POINT = 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; - final private int mType; // The type of event - one of the constants above + 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 the right type: COMMITTABLE or DEAD or TOGGLE, 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. + // 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 int codePoint, final Event next) { - mType = type; + 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; + // Sanity 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"); + } + } + } + + 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); + } + + 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} + 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); } - public static Event createDeadEvent(final int codePoint, final Event next) { - return new Event(EVENT_DEAD, codePoint, 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. + */ + 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 */); } - public static Event createCommittableEvent(final int codePoint, final Event next) { - return new Event(EVENT_COMMITTABLE, codePoint, 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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 */); } public static Event createNotHandledEvent() { - return new Event(EVENT_NOT_HANDLED, NOT_A_CODE_POINT, null); + 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); } - public boolean isCommittable() { - return EVENT_COMMITTABLE == mType; + // 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 EVENT_DEAD == mType; + return 0 != (FLAG_DEAD & mFlags); + } + + public boolean isKeyRepeat() { + return 0 != (FLAG_REPEAT & mFlags); + } + + // 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() { + switch (mEventType) { + case EVENT_TYPE_MODE_KEY: + case EVENT_TYPE_NOT_HANDLED: + case EVENT_TYPE_TOGGLE: + 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/com/android/inputmethod/event/EventDecoderSpec.java b/java/src/com/android/inputmethod/event/EventDecoderSpec.java deleted file mode 100644 index 303b4b4c9..000000000 --- a/java/src/com/android/inputmethod/event/EventDecoderSpec.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 com.android.inputmethod.event; - -/** - * Class describing a decoder chain. This will depend on the language and the input medium (soft - * or hard keyboard for example). - */ -public class EventDecoderSpec { - public EventDecoderSpec() { - } -} diff --git a/java/src/com/android/inputmethod/event/EventInterpreter.java b/java/src/com/android/inputmethod/event/EventInterpreter.java deleted file mode 100644 index 726b9206b..000000000 --- a/java/src/com/android/inputmethod/event/EventInterpreter.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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 com.android.inputmethod.event; - -import android.util.SparseArray; -import android.view.KeyEvent; - -import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.LatinIME; -import com.android.inputmethod.latin.utils.CollectionUtils; - -import java.util.ArrayList; - -/** - * This class implements the logic 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 decoding chain that starts with an event and ends up with - * a stream of code points + decoding state. - */ -public class EventInterpreter { - // TODO: Implement an object pool for events, as we'll create a lot of them - // TODO: Create a combiner - // TODO: Create an object type to represent input material + visual feedback + decoding state - // TODO: Create an interface to call back to Latin IME through the above object - - final EventDecoderSpec mDecoderSpec; - final SparseArray<HardwareEventDecoder> mHardwareEventDecoders; - final SoftwareEventDecoder mSoftwareEventDecoder; - final LatinIME mLatinIme; - final ArrayList<Combiner> mCombiners; - - /** - * Create a default interpreter. - * - * This creates a default interpreter that does nothing. A default interpreter should normally - * only be used for fallback purposes, when we really don't know what we want to do with input. - * - * @param latinIme a reference to the ime. - */ - public EventInterpreter(final LatinIME latinIme) { - this(null, latinIme); - } - - /** - * Create an event interpreter according to a specification. - * - * The specification contains information about what to do with events. Typically, it will - * contain information about the type of keyboards - for example, if hardware keyboard(s) is/are - * attached, their type will be included here so that the decoder knows what to do with each - * keypress (a 10-key keyboard is not handled like a qwerty-ish keyboard). - * It also contains information for combining characters. For example, if the input language - * is Japanese, the specification will typically request kana conversion. - * Also note that the specification can be null. This means that we need to create a default - * interpreter that does no specific combining, and assumes the most common cases. - * - * @param specification the specification for event interpretation. null for default. - * @param latinIme a reference to the ime. - */ - public EventInterpreter(final EventDecoderSpec specification, final LatinIME latinIme) { - mDecoderSpec = null != specification ? specification : new EventDecoderSpec(); - // For both, we expect to have only one decoder in almost all cases, hence the default - // capacity of 1. - mHardwareEventDecoders = new SparseArray<HardwareEventDecoder>(1); - mSoftwareEventDecoder = new SoftwareKeyboardEventDecoder(); - mCombiners = CollectionUtils.newArrayList(); - mCombiners.add(new DeadKeyCombiner()); - mLatinIme = latinIme; - } - - // Helper method to decode a hardware key event into a generic event, and execute any - // necessary action. - public boolean onHardwareKeyEvent(final KeyEvent hardwareKeyEvent) { - final Event decodedEvent = getHardwareKeyEventDecoder(hardwareKeyEvent.getDeviceId()) - .decodeHardwareKey(hardwareKeyEvent); - return onEvent(decodedEvent); - } - - public boolean onSoftwareEvent() { - final Event decodedEvent = getSoftwareEventDecoder().decodeSoftwareEvent(); - return onEvent(decodedEvent); - } - - private HardwareEventDecoder getHardwareKeyEventDecoder(final int deviceId) { - final HardwareEventDecoder decoder = mHardwareEventDecoders.get(deviceId); - if (null != decoder) return decoder; - // TODO: create the decoder according to the specification - final HardwareEventDecoder newDecoder = new HardwareKeyboardEventDecoder(deviceId); - mHardwareEventDecoders.put(deviceId, newDecoder); - return newDecoder; - } - - private SoftwareEventDecoder getSoftwareEventDecoder() { - // Within the context of Latin IME, since we never present several software interfaces - // at the time, we should never need multiple software event decoders at a time. - return mSoftwareEventDecoder; - } - - private boolean onEvent(final Event event) { - Event currentlyProcessingEvent = event; - boolean processed = false; - for (int i = 0; i < mCombiners.size(); ++i) { - currentlyProcessingEvent = mCombiners.get(i).combine(event); - } - while (null != currentlyProcessingEvent) { - if (currentlyProcessingEvent.isCommittable()) { - mLatinIme.onCodeInput(currentlyProcessingEvent.mCodePoint, - Constants.EXTERNAL_KEYBOARD_COORDINATE, - Constants.EXTERNAL_KEYBOARD_COORDINATE); - processed = true; - } else if (event.isDead()) { - processed = true; - } - currentlyProcessingEvent = currentlyProcessingEvent.mNextEvent; - } - return processed; - } -} diff --git a/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java b/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java index 720d07433..c61f45efa 100644 --- a/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java +++ b/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java @@ -46,28 +46,36 @@ public class HardwareKeyboardEventDecoder implements HardwareEventDecoder { // 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.createCommittableEvent(Constants.CODE_DELETE, null /* next */); + 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, null /* next */); + 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. - return Event.createCommittableEvent(keyEvent.isShiftPressed() - ? Constants.CODE_SHIFT_ENTER : Constants.CODE_ENTER, - null /* next */); + if (keyEvent.isShiftPressed()) { + return Event.createHardwareKeypressEvent(Event.NOT_A_CODE_POINT, + Constants.CODE_SHIFT_ENTER, null /* next */, isKeyRepeat); + } else { + return Event.createHardwareKeypressEvent(Constants.CODE_ENTER, keyCode, + null /* next */, isKeyRepeat); + } } - // If not Enter, then we have a committable character. This should be committed - // right away, taking into account the current state. - return Event.createCommittableEvent(codePointAndFlags, null /* next */); + // 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/com/android/inputmethod/event/InputTransaction.java b/java/src/com/android/inputmethod/event/InputTransaction.java new file mode 100644 index 000000000..cdff265c6 --- /dev/null +++ b/java/src/com/android/inputmethod/event/InputTransaction.java @@ -0,0 +1,100 @@ +/* + * 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 com.android.inputmethod.event; + +import com.android.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; + + 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; + } +} diff --git a/java/src/com/android/inputmethod/event/MyanmarReordering.java b/java/src/com/android/inputmethod/event/MyanmarReordering.java new file mode 100644 index 000000000..32919932d --- /dev/null +++ b/java/src/com/android/inputmethod/event/MyanmarReordering.java @@ -0,0 +1,258 @@ +/* + * 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 com.android.inputmethod.event; + +import com.android.inputmethod.latin.Constants; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * A combiner that reorders input for Myanmar. + */ +public class MyanmarReordering implements Combiner { + // U+1031 MYANMAR VOWEL SIGN E + private final static int VOWEL_E = 0x1031; // Code point for vowel E that we need to reorder + // U+200C ZERO WIDTH NON-JOINER + // U+200B ZERO WIDTH SPACE + private final static int ZERO_WIDTH_NON_JOINER = 0x200B; // should be 0x200C + + private final ArrayList<Event> mCurrentEvents = new ArrayList<>(); + + // List of consonants : + // U+1000 MYANMAR LETTER KA + // U+1001 MYANMAR LETTER KHA + // U+1002 MYANMAR LETTER GA + // U+1003 MYANMAR LETTER GHA + // U+1004 MYANMAR LETTER NGA + // U+1005 MYANMAR LETTER CA + // U+1006 MYANMAR LETTER CHA + // U+1007 MYANMAR LETTER JA + // U+1008 MYANMAR LETTER JHA + // U+1009 MYANMAR LETTER NYA + // U+100A MYANMAR LETTER NNYA + // U+100B MYANMAR LETTER TTA + // U+100C MYANMAR LETTER TTHA + // U+100D MYANMAR LETTER DDA + // U+100E MYANMAR LETTER DDHA + // U+100F MYANMAR LETTER NNA + // U+1010 MYANMAR LETTER TA + // U+1011 MYANMAR LETTER THA + // U+1012 MYANMAR LETTER DA + // U+1013 MYANMAR LETTER DHA + // U+1014 MYANMAR LETTER NA + // U+1015 MYANMAR LETTER PA + // U+1016 MYANMAR LETTER PHA + // U+1017 MYANMAR LETTER BA + // U+1018 MYANMAR LETTER BHA + // U+1019 MYANMAR LETTER MA + // U+101A MYANMAR LETTER YA + // U+101B MYANMAR LETTER RA + // U+101C MYANMAR LETTER LA + // U+101D MYANMAR LETTER WA + // U+101E MYANMAR LETTER SA + // U+101F MYANMAR LETTER HA + // U+1020 MYANMAR LETTER LLA + // U+103F MYANMAR LETTER GREAT SA + private static boolean isConsonant(final int codePoint) { + return (codePoint >= 0x1000 && codePoint <= 0x1020) || 0x103F == codePoint; + } + + // List of medials : + // U+103B MYANMAR CONSONANT SIGN MEDIAL YA + // U+103C MYANMAR CONSONANT SIGN MEDIAL RA + // U+103D MYANMAR CONSONANT SIGN MEDIAL WA + // U+103E MYANMAR CONSONANT SIGN MEDIAL HA + // U+105E MYANMAR CONSONANT SIGN MON MEDIAL NA + // U+105F MYANMAR CONSONANT SIGN MON MEDIAL MA + // U+1060 MYANMAR CONSONANT SIGN MON MEDIAL LA + // U+1082 MYANMAR CONSONANT SIGN SHAN MEDIAL WA + private static int[] MEDIAL_LIST = { 0x103B, 0x103C, 0x103D, 0x103E, + 0x105E, 0x105F, 0x1060, 0x1082}; + private static boolean isMedial(final int codePoint) { + return Arrays.binarySearch(MEDIAL_LIST, codePoint) >= 0; + } + + private static boolean isConsonantOrMedial(final int codePoint) { + return isConsonant(codePoint) || isMedial(codePoint); + } + + private Event getLastEvent() { + final int size = mCurrentEvents.size(); + if (size <= 0) { + return null; + } + return mCurrentEvents.get(size - 1); + } + + private CharSequence getCharSequence() { + final StringBuilder s = new StringBuilder(); + for (final Event e : mCurrentEvents) { + s.appendCodePoint(e.mCodePoint); + } + return s; + } + + /** + * Clears the currently combining stream of events and returns the resulting software text + * event corresponding to the stream. Optionally adds a new event to the cleared stream. + * @param newEvent the new event to add to the stream. null if none. + * @return the resulting software text event. Null if none. + */ + private Event clearAndGetResultingEvent(final Event newEvent) { + final CharSequence combinedText; + if (mCurrentEvents.size() > 0) { + combinedText = getCharSequence(); + mCurrentEvents.clear(); + } else { + combinedText = null; + } + if (null != newEvent) { + mCurrentEvents.add(newEvent); + } + return null == combinedText ? null + : Event.createSoftwareTextEvent(combinedText, Event.NOT_A_KEY_CODE); + } + + @Override + public Event processEvent(ArrayList<Event> previousEvents, Event newEvent) { + final int codePoint = newEvent.mCodePoint; + if (VOWEL_E == codePoint) { + final Event lastEvent = getLastEvent(); + if (null == lastEvent) { + mCurrentEvents.add(newEvent); + return null; + } else if (isConsonantOrMedial(lastEvent.mCodePoint)) { + final Event resultingEvent = clearAndGetResultingEvent(null); + mCurrentEvents.add(Event.createSoftwareKeypressEvent(ZERO_WIDTH_NON_JOINER, + Event.NOT_A_KEY_CODE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + false /* isKeyRepeat */)); + mCurrentEvents.add(newEvent); + return resultingEvent; + } else { // VOWEL_E == lastCodePoint. But if that was anything else this is correct too. + return clearAndGetResultingEvent(newEvent); + } + } if (isConsonant(codePoint)) { + final Event lastEvent = getLastEvent(); + if (null == lastEvent) { + mCurrentEvents.add(newEvent); + return null; + } else if (VOWEL_E == lastEvent.mCodePoint) { + final int eventSize = mCurrentEvents.size(); + if (eventSize >= 2 + && mCurrentEvents.get(eventSize - 2).mCodePoint == ZERO_WIDTH_NON_JOINER) { + // We have a ZWJN before a vowel E. We need to remove the ZWNJ and then + // reorder the vowel with respect to the consonant. + mCurrentEvents.remove(eventSize - 1); + mCurrentEvents.remove(eventSize - 2); + mCurrentEvents.add(newEvent); + mCurrentEvents.add(lastEvent); + return null; + } + // If there is already a consonant, then we are starting a new syllable. + for (int i = eventSize - 2; i >= 0; --i) { + if (isConsonant(mCurrentEvents.get(i).mCodePoint)) { + return clearAndGetResultingEvent(newEvent); + } + } + // If we come here, we didn't have a consonant so we reorder + mCurrentEvents.remove(eventSize - 1); + mCurrentEvents.add(newEvent); + mCurrentEvents.add(lastEvent); + return null; + } else { // lastCodePoint is a consonant/medial. But if it's something else it's fine + return clearAndGetResultingEvent(newEvent); + } + } else if (isMedial(codePoint)) { + final Event lastEvent = getLastEvent(); + if (null == lastEvent) { + mCurrentEvents.add(newEvent); + return null; + } else if (VOWEL_E == lastEvent.mCodePoint) { + final int eventSize = mCurrentEvents.size(); + // If there is already a consonant, then we are in the middle of a syllable, and we + // need to reorder. + boolean hasConsonant = false; + for (int i = eventSize - 2; i >= 0; --i) { + if (isConsonant(mCurrentEvents.get(i).mCodePoint)) { + hasConsonant = true; + break; + } + } + if (hasConsonant) { + mCurrentEvents.remove(eventSize - 1); + mCurrentEvents.add(newEvent); + mCurrentEvents.add(lastEvent); + return null; + } + // Otherwise, we just commit everything. + return clearAndGetResultingEvent(null); + } else { // lastCodePoint is a consonant/medial. But if it's something else it's fine + return clearAndGetResultingEvent(newEvent); + } + } else if (Constants.CODE_DELETE == newEvent.mKeyCode) { + final Event lastEvent = getLastEvent(); + final int eventSize = mCurrentEvents.size(); + if (null != lastEvent) { + if (VOWEL_E == lastEvent.mCodePoint) { + // We have a VOWEL_E at the end. There are four cases. + // - The vowel is the only code point in the buffer. Remove it. + // - The vowel is preceded by a ZWNJ. Remove both vowel E and ZWNJ. + // - The vowel is preceded by a consonant/medial, remove the consonant/medial. + // - In all other cases, it's strange, so just remove the last code point. + if (eventSize <= 1) { + mCurrentEvents.clear(); + } else { // eventSize >= 2 + final int previousCodePoint = mCurrentEvents.get(eventSize - 2).mCodePoint; + if (previousCodePoint == ZERO_WIDTH_NON_JOINER) { + mCurrentEvents.remove(eventSize - 1); + mCurrentEvents.remove(eventSize - 2); + } else if (isConsonantOrMedial(previousCodePoint)) { + mCurrentEvents.remove(eventSize - 2); + } else { + mCurrentEvents.remove(eventSize - 1); + } + } + return null; + } else if (eventSize > 0) { + mCurrentEvents.remove(eventSize - 1); + return null; + } + } + } + // This character is not part of the combining scheme, so we should reset everything. + if (mCurrentEvents.size() > 0) { + // If we have events in flight, then add the new event and return the resulting event. + mCurrentEvents.add(newEvent); + return clearAndGetResultingEvent(null); + } else { + // If we don't have any events in flight, then just pass this one through. + return newEvent; + } + } + + @Override + public CharSequence getCombiningStateFeedback() { + return getCharSequence(); + } + + @Override + public void reset() { + mCurrentEvents.clear(); + } +} diff --git a/java/src/com/android/inputmethod/event/SoftwareEventDecoder.java b/java/src/com/android/inputmethod/event/SoftwareEventDecoder.java deleted file mode 100644 index d81ee0b37..000000000 --- a/java/src/com/android/inputmethod/event/SoftwareEventDecoder.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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 com.android.inputmethod.event; - -/** - * An event decoder for events out of a software keyboard. - * - * This defines the interface for an event decoder that supports events out of a software keyboard. - * This differs significantly from hardware keyboard event decoders in several respects. First, - * a software keyboard does not have a scancode/layout system; the keypresses that insert - * characters output unicode characters directly. - */ -public interface SoftwareEventDecoder extends EventDecoder { - public Event decodeSoftwareEvent(); -} diff --git a/java/src/com/android/inputmethod/event/SoftwareKeyboardEventDecoder.java b/java/src/com/android/inputmethod/event/SoftwareKeyboardEventDecoder.java deleted file mode 100644 index de91567c7..000000000 --- a/java/src/com/android/inputmethod/event/SoftwareKeyboardEventDecoder.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 com.android.inputmethod.event; - -/** - * A decoder for events from software keyboard, like the ones displayed by Latin IME. - */ -public class SoftwareKeyboardEventDecoder implements SoftwareEventDecoder { - @Override - public Event decodeSoftwareEvent() { - return null; - } -} |