MyanmarReordering.java revision 60a2cd8ac439bf41bfddc5f5f339feda7c7ff175
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.event;
18
19import com.android.inputmethod.latin.Constants;
20
21import java.util.ArrayList;
22import java.util.Arrays;
23
24import javax.annotation.Nonnull;
25
26/**
27 * A combiner that reorders input for Myanmar.
28 */
29public class MyanmarReordering implements Combiner {
30    // U+1031 MYANMAR VOWEL SIGN E
31    private final static int VOWEL_E = 0x1031; // Code point for vowel E that we need to reorder
32    // U+200C ZERO WIDTH NON-JOINER
33    // U+200B ZERO WIDTH SPACE
34    private final static int ZERO_WIDTH_NON_JOINER = 0x200B; // should be 0x200C
35
36    private final ArrayList<Event> mCurrentEvents = new ArrayList<>();
37
38    // List of consonants :
39    // U+1000 MYANMAR LETTER KA
40    // U+1001 MYANMAR LETTER KHA
41    // U+1002 MYANMAR LETTER GA
42    // U+1003 MYANMAR LETTER GHA
43    // U+1004 MYANMAR LETTER NGA
44    // U+1005 MYANMAR LETTER CA
45    // U+1006 MYANMAR LETTER CHA
46    // U+1007 MYANMAR LETTER JA
47    // U+1008 MYANMAR LETTER JHA
48    // U+1009 MYANMAR LETTER NYA
49    // U+100A MYANMAR LETTER NNYA
50    // U+100B MYANMAR LETTER TTA
51    // U+100C MYANMAR LETTER TTHA
52    // U+100D MYANMAR LETTER DDA
53    // U+100E MYANMAR LETTER DDHA
54    // U+100F MYANMAR LETTER NNA
55    // U+1010 MYANMAR LETTER TA
56    // U+1011 MYANMAR LETTER THA
57    // U+1012 MYANMAR LETTER DA
58    // U+1013 MYANMAR LETTER DHA
59    // U+1014 MYANMAR LETTER NA
60    // U+1015 MYANMAR LETTER PA
61    // U+1016 MYANMAR LETTER PHA
62    // U+1017 MYANMAR LETTER BA
63    // U+1018 MYANMAR LETTER BHA
64    // U+1019 MYANMAR LETTER MA
65    // U+101A MYANMAR LETTER YA
66    // U+101B MYANMAR LETTER RA
67    // U+101C MYANMAR LETTER LA
68    // U+101D MYANMAR LETTER WA
69    // U+101E MYANMAR LETTER SA
70    // U+101F MYANMAR LETTER HA
71    // U+1020 MYANMAR LETTER LLA
72    // U+103F MYANMAR LETTER GREAT SA
73    private static boolean isConsonant(final int codePoint) {
74        return (codePoint >= 0x1000 && codePoint <= 0x1020) || 0x103F == codePoint;
75    }
76
77    // List of medials :
78    // U+103B MYANMAR CONSONANT SIGN MEDIAL YA
79    // U+103C MYANMAR CONSONANT SIGN MEDIAL RA
80    // U+103D MYANMAR CONSONANT SIGN MEDIAL WA
81    // U+103E MYANMAR CONSONANT SIGN MEDIAL HA
82    // U+105E MYANMAR CONSONANT SIGN MON MEDIAL NA
83    // U+105F MYANMAR CONSONANT SIGN MON MEDIAL MA
84    // U+1060 MYANMAR CONSONANT SIGN MON MEDIAL LA
85    // U+1082 MYANMAR CONSONANT SIGN SHAN MEDIAL WA
86    private static int[] MEDIAL_LIST = { 0x103B, 0x103C, 0x103D, 0x103E,
87            0x105E, 0x105F, 0x1060, 0x1082};
88    private static boolean isMedial(final int codePoint) {
89        return Arrays.binarySearch(MEDIAL_LIST, codePoint) >= 0;
90    }
91
92    private static boolean isConsonantOrMedial(final int codePoint) {
93        return isConsonant(codePoint) || isMedial(codePoint);
94    }
95
96    private Event getLastEvent() {
97        final int size = mCurrentEvents.size();
98        if (size <= 0) {
99            return null;
100        }
101        return mCurrentEvents.get(size - 1);
102    }
103
104    private CharSequence getCharSequence() {
105        final StringBuilder s = new StringBuilder();
106        for (final Event e : mCurrentEvents) {
107            s.appendCodePoint(e.mCodePoint);
108        }
109        return s;
110    }
111
112    /**
113     * Clears the currently combining stream of events and returns the resulting software text
114     * event corresponding to the stream. Optionally adds a new event to the cleared stream.
115     * @param newEvent the new event to add to the stream. null if none.
116     * @return the resulting software text event. Never null.
117     */
118    private Event clearAndGetResultingEvent(final Event newEvent) {
119        final CharSequence combinedText;
120        if (mCurrentEvents.size() > 0) {
121            combinedText = getCharSequence();
122            mCurrentEvents.clear();
123        } else {
124            combinedText = null;
125        }
126        if (null != newEvent) {
127            mCurrentEvents.add(newEvent);
128        }
129        return null == combinedText ? Event.createConsumedEvent(newEvent)
130                : Event.createSoftwareTextEvent(combinedText, Event.NOT_A_KEY_CODE);
131    }
132
133    @Nonnull
134    @Override
135    public Event processEvent(ArrayList<Event> previousEvents, Event newEvent) {
136        final int codePoint = newEvent.mCodePoint;
137        if (VOWEL_E == codePoint) {
138            final Event lastEvent = getLastEvent();
139            if (null == lastEvent) {
140                mCurrentEvents.add(newEvent);
141                return Event.createConsumedEvent(newEvent);
142            } else if (isConsonantOrMedial(lastEvent.mCodePoint)) {
143                final Event resultingEvent = clearAndGetResultingEvent(null);
144                mCurrentEvents.add(Event.createSoftwareKeypressEvent(ZERO_WIDTH_NON_JOINER,
145                        Event.NOT_A_KEY_CODE,
146                        Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE,
147                        false /* isKeyRepeat */));
148                mCurrentEvents.add(newEvent);
149                return resultingEvent;
150            } else { // VOWEL_E == lastCodePoint. But if that was anything else this is correct too.
151                return clearAndGetResultingEvent(newEvent);
152            }
153        } if (isConsonant(codePoint)) {
154            final Event lastEvent = getLastEvent();
155            if (null == lastEvent) {
156                mCurrentEvents.add(newEvent);
157                return Event.createConsumedEvent(newEvent);
158            } else if (VOWEL_E == lastEvent.mCodePoint) {
159                final int eventSize = mCurrentEvents.size();
160                if (eventSize >= 2
161                        && mCurrentEvents.get(eventSize - 2).mCodePoint == ZERO_WIDTH_NON_JOINER) {
162                    // We have a ZWJN before a vowel E. We need to remove the ZWNJ and then
163                    // reorder the vowel with respect to the consonant.
164                    mCurrentEvents.remove(eventSize - 1);
165                    mCurrentEvents.remove(eventSize - 2);
166                    mCurrentEvents.add(newEvent);
167                    mCurrentEvents.add(lastEvent);
168                    return Event.createConsumedEvent(newEvent);
169                }
170                // If there is already a consonant, then we are starting a new syllable.
171                for (int i = eventSize - 2; i >= 0; --i) {
172                    if (isConsonant(mCurrentEvents.get(i).mCodePoint)) {
173                        return clearAndGetResultingEvent(newEvent);
174                    }
175                }
176                // If we come here, we didn't have a consonant so we reorder
177                mCurrentEvents.remove(eventSize - 1);
178                mCurrentEvents.add(newEvent);
179                mCurrentEvents.add(lastEvent);
180                return Event.createConsumedEvent(newEvent);
181            } else { // lastCodePoint is a consonant/medial. But if it's something else it's fine
182                return clearAndGetResultingEvent(newEvent);
183            }
184        } else if (isMedial(codePoint)) {
185            final Event lastEvent = getLastEvent();
186            if (null == lastEvent) {
187                mCurrentEvents.add(newEvent);
188                return Event.createConsumedEvent(newEvent);
189            } else if (VOWEL_E == lastEvent.mCodePoint) {
190                final int eventSize = mCurrentEvents.size();
191                // If there is already a consonant, then we are in the middle of a syllable, and we
192                // need to reorder.
193                boolean hasConsonant = false;
194                for (int i = eventSize - 2; i >= 0; --i) {
195                    if (isConsonant(mCurrentEvents.get(i).mCodePoint)) {
196                        hasConsonant = true;
197                        break;
198                    }
199                }
200                if (hasConsonant) {
201                    mCurrentEvents.remove(eventSize - 1);
202                    mCurrentEvents.add(newEvent);
203                    mCurrentEvents.add(lastEvent);
204                    return Event.createConsumedEvent(newEvent);
205                }
206                // Otherwise, we just commit everything.
207                return clearAndGetResultingEvent(null);
208            } else { // lastCodePoint is a consonant/medial. But if it's something else it's fine
209                return clearAndGetResultingEvent(newEvent);
210            }
211        } else if (Constants.CODE_DELETE == newEvent.mKeyCode) {
212            final Event lastEvent = getLastEvent();
213            final int eventSize = mCurrentEvents.size();
214            if (null != lastEvent) {
215                if (VOWEL_E == lastEvent.mCodePoint) {
216                    // We have a VOWEL_E at the end. There are four cases.
217                    // - The vowel is the only code point in the buffer. Remove it.
218                    // - The vowel is preceded by a ZWNJ. Remove both vowel E and ZWNJ.
219                    // - The vowel is preceded by a consonant/medial, remove the consonant/medial.
220                    // - In all other cases, it's strange, so just remove the last code point.
221                    if (eventSize <= 1) {
222                        mCurrentEvents.clear();
223                    } else { // eventSize >= 2
224                        final int previousCodePoint = mCurrentEvents.get(eventSize - 2).mCodePoint;
225                        if (previousCodePoint == ZERO_WIDTH_NON_JOINER) {
226                            mCurrentEvents.remove(eventSize - 1);
227                            mCurrentEvents.remove(eventSize - 2);
228                        } else if (isConsonantOrMedial(previousCodePoint)) {
229                            mCurrentEvents.remove(eventSize - 2);
230                        } else {
231                            mCurrentEvents.remove(eventSize - 1);
232                        }
233                    }
234                    return Event.createConsumedEvent(newEvent);
235                } else if (eventSize > 0) {
236                    mCurrentEvents.remove(eventSize - 1);
237                    return Event.createConsumedEvent(newEvent);
238                }
239            }
240        }
241        // This character is not part of the combining scheme, so we should reset everything.
242        if (mCurrentEvents.size() > 0) {
243            // If we have events in flight, then add the new event and return the resulting event.
244            mCurrentEvents.add(newEvent);
245            return clearAndGetResultingEvent(null);
246        } else {
247            // If we don't have any events in flight, then just pass this one through.
248            return newEvent;
249        }
250    }
251
252    @Override
253    public CharSequence getCombiningStateFeedback() {
254        return getCharSequence();
255    }
256
257    @Override
258    public void reset() {
259        mCurrentEvents.clear();
260    }
261}
262