| /* |
| ******************************************************************************* |
| * Copyright (C) 2000-2004, International Business Machines Corporation and * |
| * others. All Rights Reserved. * |
| ******************************************************************************* |
| */ |
| |
| package com.ibm.icu.dev.tool.ime.indic; |
| |
| import java.awt.im.spi.InputMethodContext; |
| |
| import java.awt.event.KeyEvent; |
| import java.awt.event.InputMethodEvent; |
| import java.awt.font.TextAttribute; |
| import java.awt.font.TextHitInfo; |
| import java.text.AttributedCharacterIterator; |
| import java.util.Hashtable; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Set; |
| |
| class IndicInputMethodImpl { |
| |
| protected char[] KBD_MAP; |
| |
| private static final char SUBSTITUTION_BASE = '\uff00'; |
| |
| // Indexed by map value - SUBSTITUTION_BASE |
| protected char[][] SUBSTITUTION_TABLE; |
| |
| // Invalid character. |
| private static final char INVALID_CHAR = '\uffff'; |
| |
| // Unmapped versions of some interesting characters. |
| private static final char KEY_SIGN_VIRAMA = '\u0064'; // or just 'd'?? |
| private static final char KEY_SIGN_NUKTA = '\u005d'; // or just ']'?? |
| |
| // Two succeeding viramas are replaced by one virama and one ZWNJ. |
| // Viram followed by Nukta is replaced by one VIRAMA and one ZWJ |
| private static final char ZWJ = '\u200d'; |
| private static final char ZWNJ = '\u200c'; |
| |
| // Backspace |
| private static final char BACKSPACE = '\u0008'; |
| |
| // Sorted list of characters which can be followed by Nukta |
| protected char[] JOIN_WITH_NUKTA; |
| |
| // Nukta form of the above characters |
| protected char[] NUKTA_FORM; |
| |
| private int log2; |
| private int power; |
| private int extra; |
| |
| // cached TextHitInfo. Only one type of TextHitInfo is required. |
| private static final TextHitInfo ZERO_TRAILING_HIT_INFO = TextHitInfo.trailing(0); |
| |
| /** |
| * Returns the index of the given character in the JOIN_WITH_NUKTA array. |
| * If character is not found, -1 is returned. |
| */ |
| private int nuktaIndex(char ch) { |
| if (JOIN_WITH_NUKTA == null) { |
| return -1; |
| } |
| |
| int probe = power; |
| int index = 0; |
| |
| if (JOIN_WITH_NUKTA[extra] <= ch) { |
| index = extra; |
| } |
| |
| while (probe > (1 << 0)) { |
| probe >>= 1; |
| |
| if (JOIN_WITH_NUKTA[index + probe] <= ch) { |
| index += probe; |
| } |
| } |
| |
| if (JOIN_WITH_NUKTA[index] != ch) { |
| index = -1; |
| } |
| |
| return index; |
| } |
| |
| /** |
| * Returns the equivalent character for hindi locale. |
| * @param originalChar The original character. |
| */ |
| private char getMappedChar(char originalChar) { |
| if (originalChar <= KBD_MAP.length) { |
| return KBD_MAP[originalChar]; |
| } |
| |
| return originalChar; |
| } |
| |
| // Array used to hold the text to be sent. |
| // If the last character was not committed it is stored in text[0]. |
| // The variable totalChars give an indication of whether the last |
| // character was committed or not. If at any time ( but not within a |
| // a call to dispatchEvent ) totalChars is not equal to 0 ( it can |
| // only be 1 otherwise ) the last character was not committed. |
| private char [] text = new char[4]; |
| |
| // this is always 0 before and after call to dispatchEvent. This character assumes |
| // significance only within a call to dispatchEvent. |
| private int committedChars = 0;// number of committed characters |
| |
| // the total valid characters in variable text currently. |
| private int totalChars = 0;//number of total characters ( committed + composed ) |
| |
| private boolean lastCharWasVirama = false; |
| |
| private InputMethodContext context; |
| |
| // |
| // Finds the high bit by binary searching |
| // through the bits in n. |
| // |
| private static byte highBit(int n) { |
| if (n <= 0) { |
| return -32; |
| } |
| |
| byte bit = 0; |
| |
| if (n >= 1 << 16) { |
| n >>= 16; |
| bit += 16; |
| } |
| |
| if (n >= 1 << 8) { |
| n >>= 8; |
| bit += 8; |
| } |
| |
| if (n >= 1 << 4) { |
| n >>= 4; |
| bit += 4; |
| } |
| |
| if (n >= 1 << 2) { |
| n >>= 2; |
| bit += 2; |
| } |
| |
| if (n >= 1 << 1) { |
| n >>= 1; |
| bit += 1; |
| } |
| |
| return bit; |
| } |
| |
| IndicInputMethodImpl(char[] keyboardMap, char[] joinWithNukta, char[] nuktaForm, |
| char[][] substitutionTable) { |
| KBD_MAP = keyboardMap; |
| JOIN_WITH_NUKTA = joinWithNukta; |
| NUKTA_FORM = nuktaForm; |
| SUBSTITUTION_TABLE = substitutionTable; |
| |
| if (JOIN_WITH_NUKTA != null) { |
| int log2 = highBit(JOIN_WITH_NUKTA.length); |
| |
| power = 1 << log2; |
| extra = JOIN_WITH_NUKTA.length - power; |
| } else { |
| power = extra = 0; |
| } |
| |
| } |
| |
| void setInputMethodContext(InputMethodContext context) { |
| this.context = context; |
| } |
| |
| void handleKeyTyped(KeyEvent kevent) { |
| char keyChar = kevent.getKeyChar(); |
| char currentChar = getMappedChar(keyChar); |
| |
| // The Explicit and Soft Halanta case. |
| if ( lastCharWasVirama ) { |
| switch (keyChar) { |
| case KEY_SIGN_NUKTA: |
| currentChar = ZWJ; |
| break; |
| case KEY_SIGN_VIRAMA: |
| currentChar = ZWNJ; |
| break; |
| default: |
| }//endSwitch |
| }//endif |
| |
| if (currentChar == INVALID_CHAR) { |
| kevent.consume(); |
| return; |
| } |
| |
| if (currentChar == BACKSPACE) { |
| lastCharWasVirama = false; |
| |
| if (totalChars > 0) { |
| totalChars = committedChars = 0; |
| } else { |
| return; |
| } |
| } |
| else if (keyChar == KEY_SIGN_NUKTA) { |
| int nuktaIndex = nuktaIndex(text[0]); |
| |
| if (nuktaIndex != -1) { |
| text[0] = NUKTA_FORM[nuktaIndex]; |
| } else { |
| // the last character was committed, commit just Nukta. |
| // Note : the lastChar must have been committed if it is not one of |
| // the characters which combine with nukta. |
| // the state must be totalChars = committedChars = 0; |
| text[totalChars++] = currentChar; |
| } |
| |
| committedChars += 1; |
| } |
| else { |
| int nuktaIndex = nuktaIndex(currentChar); |
| |
| if (nuktaIndex != -1) { |
| // Commit everything but currentChar |
| text[totalChars++] = currentChar; |
| committedChars = totalChars-1; |
| } else { |
| if (currentChar >= SUBSTITUTION_BASE) { |
| char[] sub = SUBSTITUTION_TABLE[currentChar - SUBSTITUTION_BASE]; |
| |
| System.arraycopy(sub, 0, text, totalChars, sub.length); |
| totalChars += sub.length; |
| } else { |
| text[totalChars++] = currentChar; |
| } |
| |
| committedChars = totalChars; |
| } |
| } |
| |
| ACIText aText = new ACIText( text, 0, totalChars, committedChars ); |
| int composedCharLength = totalChars - committedChars; |
| TextHitInfo caret=null,visiblePosition=null; |
| switch( composedCharLength ) { |
| case 0: |
| break; |
| case 1: |
| visiblePosition = caret = ZERO_TRAILING_HIT_INFO; |
| break; |
| default: |
| // The code should not reach here. There is no case where there can be |
| // more than one character pending. |
| } |
| |
| context.dispatchInputMethodEvent(InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, |
| aText, |
| committedChars, |
| caret, |
| visiblePosition); |
| |
| if (totalChars == 0) { |
| text[0] = INVALID_CHAR; |
| } else { |
| text[0] = text[totalChars - 1];// make text[0] hold the last character |
| } |
| |
| lastCharWasVirama = keyChar == KEY_SIGN_VIRAMA && !lastCharWasVirama; |
| |
| totalChars -= committedChars; |
| committedChars = 0; |
| // state now text[0] = last character |
| // totalChars = ( last character committed )? 0 : 1; |
| // committedChars = 0; |
| |
| kevent.consume();// prevent client from getting this event. |
| } |
| |
| void endComposition() { |
| if( totalChars != 0 ) {// if some character is not committed. |
| ACIText aText = new ACIText( text, 0, totalChars, totalChars ); |
| context.dispatchInputMethodEvent( InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, |
| aText, totalChars, null, null ); |
| totalChars = committedChars = 0; |
| text[0] = INVALID_CHAR; |
| lastCharWasVirama = false; |
| } |
| } |
| |
| // custom AttributedCharacterIterator -- much lightweight since currently there is no |
| // attribute defined on the text being generated by the input method. |
| private class ACIText implements AttributedCharacterIterator { |
| private char [] text = null; |
| private int committed = 0; |
| private int index = 0; |
| |
| ACIText( char [] chArray, int offset, int length, int committed ) { |
| this.text = new char[length]; |
| this.committed = committed; |
| System.arraycopy( chArray, offset, text, 0, length ); |
| } |
| |
| // CharacterIterator methods. |
| public char first() { |
| return _setIndex( 0 ); |
| } |
| |
| public char last() { |
| if( text.length == 0 ) { |
| return _setIndex( text.length ); |
| } |
| return _setIndex( text.length - 1 ); |
| } |
| |
| public char current() { |
| if( index == text.length ) |
| return DONE; |
| return text[index]; |
| } |
| |
| public char next() { |
| if( index == text.length ) { |
| return DONE; |
| } |
| return _setIndex( index + 1 ); |
| } |
| |
| public char previous() { |
| if( index == 0 ) |
| return DONE; |
| return _setIndex( index - 1 ); |
| } |
| |
| public char setIndex(int position) { |
| if( position < 0 || position > text.length ) { |
| throw new IllegalArgumentException(); |
| } |
| return _setIndex( position ); |
| } |
| |
| public int getBeginIndex() { |
| return 0; |
| } |
| |
| public int getEndIndex() { |
| return text.length; |
| } |
| |
| public int getIndex() { |
| return index; |
| } |
| |
| public Object clone() { |
| try { |
| ACIText clone = (ACIText) super.clone(); |
| return clone; |
| } catch (CloneNotSupportedException e) { |
| throw new InternalError(); |
| } |
| } |
| |
| // AttributedCharacterIterator methods. |
| public int getRunStart() { |
| return index >= committed ? committed : 0; |
| } |
| |
| public int getRunStart(AttributedCharacterIterator.Attribute attribute) { |
| return (index >= committed && |
| attribute == TextAttribute.INPUT_METHOD_UNDERLINE) ? committed : 0; |
| } |
| |
| public int getRunStart(Set attributes) { |
| return (index >= committed && |
| attributes.contains(TextAttribute.INPUT_METHOD_UNDERLINE)) ? committed : 0; |
| } |
| |
| public int getRunLimit() { |
| return index < committed ? committed : text.length; |
| } |
| |
| public int getRunLimit(AttributedCharacterIterator.Attribute attribute) { |
| return (index < committed && |
| attribute == TextAttribute.INPUT_METHOD_UNDERLINE) ? committed : text.length; |
| } |
| |
| public int getRunLimit(Set attributes) { |
| return (index < committed && |
| attributes.contains(TextAttribute.INPUT_METHOD_UNDERLINE)) ? committed : text.length; |
| } |
| |
| public Map getAttributes() { |
| Hashtable result = new Hashtable(); |
| if (index >= committed && committed < text.length) { |
| result.put(TextAttribute.INPUT_METHOD_UNDERLINE, |
| TextAttribute.UNDERLINE_LOW_ONE_PIXEL); |
| } |
| return result; |
| } |
| |
| public Object getAttribute(AttributedCharacterIterator.Attribute attribute) { |
| if (index >= committed && |
| committed < text.length && |
| attribute == TextAttribute.INPUT_METHOD_UNDERLINE) { |
| |
| return TextAttribute.UNDERLINE_LOW_ONE_PIXEL; |
| } |
| return null; |
| } |
| |
| public Set getAllAttributeKeys() { |
| HashSet result = new HashSet(); |
| if (committed < text.length) { |
| result.add(TextAttribute.INPUT_METHOD_UNDERLINE); |
| } |
| return result; |
| } |
| |
| // private methods |
| |
| /** |
| * This is always called with valid i ( 0 < i <= text.length ) |
| */ |
| private char _setIndex( int i ) { |
| index = i; |
| if( i == text.length ) { |
| return DONE; |
| } |
| return text[i]; |
| } |
| |
| } |
| } |