001: /*
002: *******************************************************************************
003: * Copyright (C) 2000-2006, International Business Machines Corporation and *
004: * others. All Rights Reserved. *
005: *******************************************************************************
006: */
007:
008: package com.ibm.icu.dev.tool.ime.indic;
009:
010: import java.awt.im.spi.InputMethodContext;
011:
012: import java.awt.event.KeyEvent;
013: import java.awt.event.InputMethodEvent;
014: import java.awt.font.TextAttribute;
015: import java.awt.font.TextHitInfo;
016: import java.text.AttributedCharacterIterator;
017: import java.util.Hashtable;
018: import java.util.HashSet;
019: import java.util.Map;
020: import java.util.Set;
021:
022: class IndicInputMethodImpl {
023:
024: protected char[] KBD_MAP;
025:
026: private static final char SUBSTITUTION_BASE = '\uff00';
027:
028: // Indexed by map value - SUBSTITUTION_BASE
029: protected char[][] SUBSTITUTION_TABLE;
030:
031: // Invalid character.
032: private static final char INVALID_CHAR = '\uffff';
033:
034: // Unmapped versions of some interesting characters.
035: private static final char KEY_SIGN_VIRAMA = '\u0064'; // or just 'd'??
036: private static final char KEY_SIGN_NUKTA = '\u005d'; // or just ']'??
037:
038: // Two succeeding viramas are replaced by one virama and one ZWNJ.
039: // Viram followed by Nukta is replaced by one VIRAMA and one ZWJ
040: private static final char ZWJ = '\u200d';
041: private static final char ZWNJ = '\u200c';
042:
043: // Backspace
044: private static final char BACKSPACE = '\u0008';
045:
046: // Sorted list of characters which can be followed by Nukta
047: protected char[] JOIN_WITH_NUKTA;
048:
049: // Nukta form of the above characters
050: protected char[] NUKTA_FORM;
051:
052: private int log2;
053: private int power;
054: private int extra;
055:
056: // cached TextHitInfo. Only one type of TextHitInfo is required.
057: private static final TextHitInfo ZERO_TRAILING_HIT_INFO = TextHitInfo
058: .trailing(0);
059:
060: /**
061: * Returns the index of the given character in the JOIN_WITH_NUKTA array.
062: * If character is not found, -1 is returned.
063: */
064: private int nuktaIndex(char ch) {
065: if (JOIN_WITH_NUKTA == null) {
066: return -1;
067: }
068:
069: int probe = power;
070: int index = 0;
071:
072: if (JOIN_WITH_NUKTA[extra] <= ch) {
073: index = extra;
074: }
075:
076: while (probe > (1 << 0)) {
077: probe >>= 1;
078:
079: if (JOIN_WITH_NUKTA[index + probe] <= ch) {
080: index += probe;
081: }
082: }
083:
084: if (JOIN_WITH_NUKTA[index] != ch) {
085: index = -1;
086: }
087:
088: return index;
089: }
090:
091: /**
092: * Returns the equivalent character for hindi locale.
093: * @param originalChar The original character.
094: */
095: private char getMappedChar(char originalChar) {
096: if (originalChar <= KBD_MAP.length) {
097: return KBD_MAP[originalChar];
098: }
099:
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,
162: char[] nuktaForm, 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: } else if (keyChar == KEY_SIGN_NUKTA) {
214: int nuktaIndex = nuktaIndex(text[0]);
215:
216: if (nuktaIndex != -1) {
217: text[0] = NUKTA_FORM[nuktaIndex];
218: } else {
219: // the last character was committed, commit just Nukta.
220: // Note : the lastChar must have been committed if it is not one of
221: // the characters which combine with nukta.
222: // the state must be totalChars = committedChars = 0;
223: text[totalChars++] = currentChar;
224: }
225:
226: committedChars += 1;
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
237: - SUBSTITUTION_BASE];
238:
239: System.arraycopy(sub, 0, text, totalChars,
240: 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(
265: InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, aText,
266: committedChars, caret, 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
275: && !lastCharWasVirama;
276:
277: totalChars -= committedChars;
278: committedChars = 0;
279: // state now text[0] = last character
280: // totalChars = ( last character committed )? 0 : 1;
281: // committedChars = 0;
282:
283: kevent.consume();// prevent client from getting this event.
284: }
285:
286: void endComposition() {
287: if (totalChars != 0) {// if some character is not committed.
288: ACIText aText = new ACIText(text, 0, totalChars, totalChars);
289: context.dispatchInputMethodEvent(
290: InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, aText,
291: 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(
376: AttributedCharacterIterator.Attribute attribute) {
377: return (index >= committed && attribute == TextAttribute.INPUT_METHOD_UNDERLINE) ? committed
378: : 0;
379: }
380:
381: public int getRunStart(Set attributes) {
382: return (index >= committed && attributes
383: .contains(TextAttribute.INPUT_METHOD_UNDERLINE)) ? committed
384: : 0;
385: }
386:
387: public int getRunLimit() {
388: return index < committed ? committed : text.length;
389: }
390:
391: public int getRunLimit(
392: AttributedCharacterIterator.Attribute attribute) {
393: return (index < committed && attribute == TextAttribute.INPUT_METHOD_UNDERLINE) ? committed
394: : text.length;
395: }
396:
397: public int getRunLimit(Set attributes) {
398: return (index < committed && attributes
399: .contains(TextAttribute.INPUT_METHOD_UNDERLINE)) ? committed
400: : text.length;
401: }
402:
403: public Map getAttributes() {
404: Hashtable result = new Hashtable();
405: if (index >= committed && committed < text.length) {
406: result.put(TextAttribute.INPUT_METHOD_UNDERLINE,
407: TextAttribute.UNDERLINE_LOW_ONE_PIXEL);
408: }
409: return result;
410: }
411:
412: public Object getAttribute(
413: AttributedCharacterIterator.Attribute attribute) {
414: if (index >= committed
415: && committed < text.length
416: && attribute == TextAttribute.INPUT_METHOD_UNDERLINE) {
417:
418: return TextAttribute.UNDERLINE_LOW_ONE_PIXEL;
419: }
420: return null;
421: }
422:
423: public Set getAllAttributeKeys() {
424: HashSet result = new HashSet();
425: if (committed < text.length) {
426: result.add(TextAttribute.INPUT_METHOD_UNDERLINE);
427: }
428: return result;
429: }
430:
431: // private methods
432:
433: /**
434: * This is always called with valid i ( 0 < i <= text.length )
435: */
436: private char _setIndex(int i) {
437: index = i;
438: if (i == text.length) {
439: return DONE;
440: }
441: return text[i];
442: }
443:
444: }
445: }
|