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