Home | History | Annotate | Download | only in indic
      1 //  2016 and later: Unicode, Inc. and others.
      2 // License & terms of use: http://www.unicode.org/copyright.html#License
      3 /*
      4  *******************************************************************************
      5  * Copyright (C) 2000-2010, International Business Machines Corporation and    *
      6  * others. All Rights Reserved.                                                *
      7  *******************************************************************************
      8  */
      9 
     10 package com.ibm.icu.dev.tool.ime.indic;
     11 
     12 import java.awt.event.InputMethodEvent;
     13 import java.awt.event.KeyEvent;
     14 import java.awt.font.TextAttribute;
     15 import java.awt.font.TextHitInfo;
     16 import java.awt.im.spi.InputMethodContext;
     17 import java.text.AttributedCharacterIterator;
     18 import java.util.HashSet;
     19 import java.util.Hashtable;
     20 import java.util.Map;
     21 import java.util.Set;
     22 
     23 class IndicInputMethodImpl {
     24 
     25     protected char[] KBD_MAP;
     26 
     27     private static final char SUBSTITUTION_BASE = '\uff00';
     28 
     29     // Indexed by map value - SUBSTITUTION_BASE
     30     protected char[][] SUBSTITUTION_TABLE;
     31 
     32     // Invalid character.
     33     private static final char INVALID_CHAR              = '\uffff';
     34 
     35     // Unmapped versions of some interesting characters.
     36     private static final char KEY_SIGN_VIRAMA           = '\u0064'; // or just 'd'??
     37     private static final char KEY_SIGN_NUKTA            = '\u005d';  // or just ']'??
     38 
     39     // Two succeeding viramas are replaced by one virama and one ZWNJ.
     40     // Viram followed by Nukta is replaced by one VIRAMA and one ZWJ
     41     private static final char ZWJ                       = '\u200d';
     42     private static final char ZWNJ                      = '\u200c';
     43 
     44     // Backspace
     45     private static final char BACKSPACE                 = '\u0008';
     46 
     47     // Sorted list of characters which can be followed by Nukta
     48     protected char[] JOIN_WITH_NUKTA;
     49 
     50     // Nukta form of the above characters
     51     protected char[] NUKTA_FORM;
     52 
     53     //private int log2;
     54     private int power;
     55     private int extra;
     56 
     57     // cached TextHitInfo. Only one type of TextHitInfo is required.
     58     private static final TextHitInfo ZERO_TRAILING_HIT_INFO = TextHitInfo.trailing(0);
     59 
     60     /**
     61      * Returns the index of the given character in the JOIN_WITH_NUKTA array.
     62      * If character is not found, -1 is returned.
     63      */
     64     private int nuktaIndex(char ch) {
     65         if (JOIN_WITH_NUKTA == null) {
     66             return -1;
     67         }
     68 
     69         int probe = power;
     70         int index = 0;
     71 
     72         if (JOIN_WITH_NUKTA[extra] <= ch) {
     73             index = extra;
     74         }
     75 
     76         while (probe > (1 << 0)) {
     77             probe >>= 1;
     78 
     79             if (JOIN_WITH_NUKTA[index + probe] <= ch) {
     80                 index += probe;
     81             }
     82         }
     83 
     84         if (JOIN_WITH_NUKTA[index] != ch) {
     85             index = -1;
     86         }
     87 
     88         return index;
     89     }
     90 
     91     /**
     92      * Returns the equivalent character for hindi locale.
     93      * @param originalChar The original character.
     94      */
     95     private char getMappedChar(char originalChar) {
     96         if (originalChar <= KBD_MAP.length) {
     97             return KBD_MAP[originalChar];
     98         }
     99 
    100         return originalChar;
    101     }
    102 
    103     // Array used to hold the text to be sent.
    104     // If the last character was not committed it is stored in text[0].
    105     // The variable totalChars give an indication of whether the last
    106     // character was committed or not. If at any time ( but not within a
    107     // a call to dispatchEvent ) totalChars is not equal to 0 ( it can
    108     // only be 1 otherwise ) the last character was not committed.
    109     private char [] text = new char[4];
    110 
    111     // this is always 0 before and after call to dispatchEvent. This character assumes
    112     // significance only within a call to dispatchEvent.
    113     private int committedChars = 0;// number of committed characters
    114 
    115     // the total valid characters in variable text currently.
    116     private int totalChars = 0;//number of total characters ( committed + composed )
    117 
    118     private boolean lastCharWasVirama = false;
    119 
    120     private InputMethodContext context;
    121 
    122     //
    123     // Finds the high bit by binary searching
    124     // through the bits in n.
    125     //
    126     private static byte highBit(int n) {
    127         if (n <= 0) {
    128             return -32;
    129         }
    130 
    131         byte bit = 0;
    132 
    133         if (n >= 1 << 16) {
    134             n >>= 16;
    135             bit += 16;
    136         }
    137 
    138         if (n >= 1 << 8) {
    139             n >>= 8;
    140             bit += 8;
    141         }
    142 
    143         if (n >= 1 << 4) {
    144             n >>= 4;
    145             bit += 4;
    146         }
    147 
    148         if (n >= 1 << 2) {
    149             n >>= 2;
    150             bit += 2;
    151         }
    152 
    153         if (n >= 1 << 1) {
    154             n >>= 1;
    155             bit += 1;
    156         }
    157 
    158         return bit;
    159     }
    160 
    161     IndicInputMethodImpl(char[] keyboardMap, char[] joinWithNukta, char[] nuktaForm,
    162                          char[][] substitutionTable) {
    163         KBD_MAP = keyboardMap;
    164         JOIN_WITH_NUKTA = joinWithNukta;
    165         NUKTA_FORM = nuktaForm;
    166         SUBSTITUTION_TABLE = substitutionTable;
    167 
    168         if (JOIN_WITH_NUKTA != null) {
    169             int log2 = highBit(JOIN_WITH_NUKTA.length);
    170 
    171             power = 1 << log2;
    172             extra = JOIN_WITH_NUKTA.length - power;
    173         } else {
    174             power = extra = 0;
    175         }
    176 
    177     }
    178 
    179     void setInputMethodContext(InputMethodContext context) {
    180         this.context = context;
    181     }
    182 
    183     void handleKeyTyped(KeyEvent kevent) {
    184         char keyChar = kevent.getKeyChar();
    185         char currentChar = getMappedChar(keyChar);
    186 
    187         // The Explicit and Soft Halanta case.
    188         if ( lastCharWasVirama ) {
    189             switch (keyChar) {
    190             case KEY_SIGN_NUKTA:
    191                 currentChar = ZWJ;
    192                 break;
    193             case KEY_SIGN_VIRAMA:
    194                 currentChar = ZWNJ;
    195                 break;
    196             default:
    197             }//endSwitch
    198         }//endif
    199 
    200         if (currentChar == INVALID_CHAR) {
    201             kevent.consume();
    202             return;
    203         }
    204 
    205         if (currentChar == BACKSPACE) {
    206             lastCharWasVirama = false;
    207 
    208             if (totalChars > 0) {
    209                 totalChars = committedChars = 0;
    210             } else {
    211                 return;
    212             }
    213         }
    214         else if (keyChar == KEY_SIGN_NUKTA) {
    215             int nuktaIndex = nuktaIndex(text[0]);
    216 
    217             if (nuktaIndex != -1) {
    218                 text[0] = NUKTA_FORM[nuktaIndex];
    219             } else {
    220                 // the last character was committed, commit just Nukta.
    221                 // Note : the lastChar must have been committed if it is not one of
    222                 // the characters which combine with nukta.
    223                 // the state must be totalChars = committedChars = 0;
    224                 text[totalChars++] = currentChar;
    225             }
    226 
    227             committedChars += 1;
    228         }
    229         else {
    230             int nuktaIndex = nuktaIndex(currentChar);
    231 
    232             if (nuktaIndex != -1) {
    233                 // Commit everything but currentChar
    234                 text[totalChars++] = currentChar;
    235                 committedChars = totalChars-1;
    236             } else {
    237                 if (currentChar >= SUBSTITUTION_BASE) {
    238                     char[] sub = SUBSTITUTION_TABLE[currentChar - SUBSTITUTION_BASE];
    239 
    240                     System.arraycopy(sub, 0, text, totalChars, sub.length);
    241                     totalChars += sub.length;
    242                 } else {
    243                     text[totalChars++] = currentChar;
    244                 }
    245 
    246                 committedChars = totalChars;
    247             }
    248         }
    249 
    250         ACIText aText = new ACIText( text, 0, totalChars, committedChars );
    251         int composedCharLength = totalChars - committedChars;
    252         TextHitInfo caret=null,visiblePosition=null;
    253         switch( composedCharLength ) {
    254             case 0:
    255                 break;
    256             case 1:
    257                 visiblePosition = caret = ZERO_TRAILING_HIT_INFO;
    258                 break;
    259             default:
    260                 // The code should not reach here. There is no case where there can be
    261                 // more than one character pending.
    262         }
    263 
    264         context.dispatchInputMethodEvent(InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
    265                                          aText,
    266                                          committedChars,
    267                                          caret,
    268                                          visiblePosition);
    269 
    270         if (totalChars == 0) {
    271             text[0] = INVALID_CHAR;
    272         } else {
    273             text[0] = text[totalChars - 1];// make text[0] hold the last character
    274         }
    275 
    276         lastCharWasVirama =  keyChar == KEY_SIGN_VIRAMA && !lastCharWasVirama;
    277 
    278         totalChars -= committedChars;
    279         committedChars = 0;
    280         // state now text[0] = last character
    281         // totalChars = ( last character committed )? 0 : 1;
    282         // committedChars = 0;
    283 
    284         kevent.consume();// prevent client from getting this event.
    285     }
    286 
    287     void endComposition() {
    288         if( totalChars != 0 ) {// if some character is not committed.
    289             ACIText aText = new ACIText( text, 0, totalChars, totalChars );
    290             context.dispatchInputMethodEvent( InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
    291                                 aText, totalChars, null, null );
    292             totalChars = committedChars = 0;
    293             text[0] = INVALID_CHAR;
    294             lastCharWasVirama = false;
    295         }
    296     }
    297 
    298     // custom AttributedCharacterIterator -- much lightweight since currently there is no
    299     // attribute defined on the text being generated by the input method.
    300     private class ACIText implements AttributedCharacterIterator {
    301         private char [] text = null;
    302         private int committed = 0;
    303         private int index = 0;
    304 
    305         ACIText( char [] chArray, int offset, int length, int committed ) {
    306             this.text = new char[length];
    307             this.committed = committed;
    308             System.arraycopy( chArray, offset, text, 0, length );
    309         }
    310 
    311         // CharacterIterator methods.
    312         public char first() {
    313             return _setIndex( 0 );
    314         }
    315 
    316         public char last() {
    317             if( text.length == 0 ) {
    318                 return _setIndex( text.length );
    319             }
    320             return _setIndex( text.length - 1 );
    321         }
    322 
    323         public char current() {
    324             if( index == text.length )
    325                 return DONE;
    326             return text[index];
    327         }
    328 
    329         public char next() {
    330             if( index == text.length ) {
    331                 return DONE;
    332             }
    333             return _setIndex( index + 1 );
    334         }
    335 
    336         public char previous() {
    337             if( index == 0 )
    338                 return DONE;
    339             return _setIndex( index - 1 );
    340         }
    341 
    342         public char setIndex(int position) {
    343             if( position < 0 || position > text.length ) {
    344                 throw new IllegalArgumentException();
    345             }
    346             return _setIndex( position );
    347         }
    348 
    349         public int getBeginIndex() {
    350             return 0;
    351         }
    352 
    353         public int getEndIndex() {
    354             return text.length;
    355         }
    356 
    357         public int getIndex() {
    358             return index;
    359         }
    360 
    361         public Object clone() {
    362             try {
    363                 ACIText clone = (ACIText) super.clone();
    364                 return clone;
    365             } catch (CloneNotSupportedException e) {
    366                 throw new IllegalStateException();
    367             }
    368         }
    369 
    370         // AttributedCharacterIterator methods.
    371         public int getRunStart() {
    372             return index >= committed ? committed : 0;
    373         }
    374 
    375         public int getRunStart(AttributedCharacterIterator.Attribute attribute) {
    376             return (index >= committed &&
    377                 attribute == TextAttribute.INPUT_METHOD_UNDERLINE) ? committed : 0;
    378         }
    379 
    380         public int getRunStart(Set attributes) {
    381             return (index >= committed &&
    382                     attributes.contains(TextAttribute.INPUT_METHOD_UNDERLINE)) ? committed : 0;
    383         }
    384 
    385         public int getRunLimit() {
    386             return index < committed ? committed : text.length;
    387         }
    388 
    389         public int getRunLimit(AttributedCharacterIterator.Attribute attribute) {
    390             return (index < committed &&
    391                     attribute == TextAttribute.INPUT_METHOD_UNDERLINE) ? committed : text.length;
    392         }
    393 
    394         public int getRunLimit(Set attributes) {
    395             return (index < committed &&
    396                     attributes.contains(TextAttribute.INPUT_METHOD_UNDERLINE)) ? committed : text.length;
    397         }
    398 
    399         public Map getAttributes() {
    400             Hashtable result = new Hashtable();
    401             if (index >= committed && committed < text.length) {
    402                 result.put(TextAttribute.INPUT_METHOD_UNDERLINE,
    403                            TextAttribute.UNDERLINE_LOW_ONE_PIXEL);
    404             }
    405             return result;
    406         }
    407 
    408         public Object getAttribute(AttributedCharacterIterator.Attribute attribute) {
    409             if (index >= committed &&
    410                 committed < text.length &&
    411                 attribute == TextAttribute.INPUT_METHOD_UNDERLINE) {
    412 
    413                 return TextAttribute.UNDERLINE_LOW_ONE_PIXEL;
    414             }
    415             return null;
    416         }
    417 
    418         public Set getAllAttributeKeys() {
    419             HashSet result = new HashSet();
    420             if (committed < text.length) {
    421                 result.add(TextAttribute.INPUT_METHOD_UNDERLINE);
    422             }
    423             return result;
    424         }
    425 
    426         // private methods
    427 
    428         /**
    429          * This is always called with valid i ( 0 < i <= text.length )
    430          */
    431         private char _setIndex( int i ) {
    432             index = i;
    433             if( i == text.length ) {
    434                 return DONE;
    435             }
    436             return text[i];
    437         }
    438 
    439     }
    440 }
    441