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