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