• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.inputmethod.event;
18 
19 import com.android.inputmethod.latin.Constants;
20 
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 
24 import javax.annotation.Nonnull;
25 
26 /**
27  * A combiner that reorders input for Myanmar.
28  */
29 public 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
isConsonant(final int codePoint)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};
isMedial(final int codePoint)88     private static boolean isMedial(final int codePoint) {
89         return Arrays.binarySearch(MEDIAL_LIST, codePoint) >= 0;
90     }
91 
isConsonantOrMedial(final int codePoint)92     private static boolean isConsonantOrMedial(final int codePoint) {
93         return isConsonant(codePoint) || isMedial(codePoint);
94     }
95 
getLastEvent()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 
getCharSequence()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      */
clearAndGetResultingEvent(final Event newEvent)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     @Override
134     @Nonnull
processEvent(ArrayList<Event> previousEvents, Event newEvent)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
getCombiningStateFeedback()253     public CharSequence getCombiningStateFeedback() {
254         return getCharSequence();
255     }
256 
257     @Override
reset()258     public void reset() {
259         mCurrentEvents.clear();
260     }
261 }
262