Home | History | Annotate | Download | only in event
      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
     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     @Override
    134     @Nonnull
    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