xref: /aosp_15_r20/external/icu/icu4j/tools/misc/src/main/java/com/ibm/icu/dev/tool/ime/indic/IndicInputMethodImpl.java (revision 0e209d3975ff4a8c132096b14b0e9364a753506e)
1 // © 2016 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html
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      */
nuktaIndex(char ch)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      */
getMappedChar(char originalChar)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     //
highBit(int n)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 
IndicInputMethodImpl(char[] keyboardMap, char[] joinWithNukta, char[] nuktaForm, char[][] substitutionTable)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 
setInputMethodContext(InputMethodContext context)179     void setInputMethodContext(InputMethodContext context) {
180         this.context = context;
181     }
182 
handleKeyTyped(KeyEvent kevent)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 
endComposition()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 
ACIText( char [] chArray, int offset, int length, int committed )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.
first()312         public char first() {
313             return _setIndex( 0 );
314         }
315 
last()316         public char last() {
317             if( text.length == 0 ) {
318                 return _setIndex( text.length );
319             }
320             return _setIndex( text.length - 1 );
321         }
322 
current()323         public char current() {
324             if( index == text.length )
325                 return DONE;
326             return text[index];
327         }
328 
next()329         public char next() {
330             if( index == text.length ) {
331                 return DONE;
332             }
333             return _setIndex( index + 1 );
334         }
335 
previous()336         public char previous() {
337             if( index == 0 )
338                 return DONE;
339             return _setIndex( index - 1 );
340         }
341 
setIndex(int position)342         public char setIndex(int position) {
343             if( position < 0 || position > text.length ) {
344                 throw new IllegalArgumentException();
345             }
346             return _setIndex( position );
347         }
348 
getBeginIndex()349         public int getBeginIndex() {
350             return 0;
351         }
352 
getEndIndex()353         public int getEndIndex() {
354             return text.length;
355         }
356 
getIndex()357         public int getIndex() {
358             return index;
359         }
360 
clone()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.
getRunStart()371         public int getRunStart() {
372             return index >= committed ? committed : 0;
373         }
374 
getRunStart(AttributedCharacterIterator.Attribute attribute)375         public int getRunStart(AttributedCharacterIterator.Attribute attribute) {
376             return (index >= committed &&
377                 attribute == TextAttribute.INPUT_METHOD_UNDERLINE) ? committed : 0;
378         }
379 
getRunStart(Set attributes)380         public int getRunStart(Set attributes) {
381             return (index >= committed &&
382                     attributes.contains(TextAttribute.INPUT_METHOD_UNDERLINE)) ? committed : 0;
383         }
384 
getRunLimit()385         public int getRunLimit() {
386             return index < committed ? committed : text.length;
387         }
388 
getRunLimit(AttributedCharacterIterator.Attribute attribute)389         public int getRunLimit(AttributedCharacterIterator.Attribute attribute) {
390             return (index < committed &&
391                     attribute == TextAttribute.INPUT_METHOD_UNDERLINE) ? committed : text.length;
392         }
393 
getRunLimit(Set attributes)394         public int getRunLimit(Set attributes) {
395             return (index < committed &&
396                     attributes.contains(TextAttribute.INPUT_METHOD_UNDERLINE)) ? committed : text.length;
397         }
398 
getAttributes()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 
getAttribute(AttributedCharacterIterator.Attribute attribute)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 
getAllAttributeKeys()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          */
_setIndex( int i )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