1 /*
2  * Copyright (c) 2002, 2013, Oracle and/or its affiliates. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  *   - Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer.
10  *
11  *   - Redistributions in binary form must reproduce the above copyright
12  *     notice, this list of conditions and the following disclaimer in the
13  *     documentation and/or other materials provided with the distribution.
14  *
15  *   - Neither the name of Oracle nor the names of its
16  *     contributors may be used to endorse or promote products derived
17  *     from this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
20  * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
21  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
22  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
23  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
24  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
25  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31 
32 /*
33  * This source code is provided to illustrate the usage of a given feature
34  * or technique and has been deliberately simplified. Additional steps
35  * required for a production-quality application, such as security checks,
36  * input validation and proper error handling, might not be present in
37  * this sample code.
38  */
39 
40 package com.sun.inputmethods.internal.codepointim;
41 
42 
43 import java.awt.AWTEvent;
44 import java.awt.Toolkit;
45 import java.awt.Rectangle;
46 import java.awt.event.InputMethodEvent;
47 import java.awt.event.KeyEvent;
48 import java.awt.font.TextAttribute;
49 import java.awt.font.TextHitInfo;
50 import java.awt.im.InputMethodHighlight;
51 import java.awt.im.spi.InputMethod;
52 import java.awt.im.spi.InputMethodContext;
53 import java.io.IOException;
54 import java.text.AttributedString;
55 import java.util.Locale;
56 
57 
58 /**
59  * The Code Point Input Method is a simple input method that allows Unicode
60  * characters to be entered using their code point or code unit values. See the
61  * accompanying file README.txt for more information.
62  *
63  * @author Brian Beck
64  */
65 public class CodePointInputMethod implements InputMethod {
66 
67     private static final int UNSET = 0;
68     private static final int ESCAPE = 1; // \u0000       - \uFFFF
69     private static final int SPECIAL_ESCAPE = 2; // \U000000     - \U10FFFF
70     private static final int SURROGATE_PAIR = 3; // \uD800\uDC00 - \uDBFF\uDFFF
71     private InputMethodContext context;
72     private Locale locale;
73     private StringBuffer buffer;
74     private int insertionPoint;
75     private int format = UNSET;
76 
CodePointInputMethod()77     public CodePointInputMethod() throws IOException {
78     }
79 
80     /**
81      * This is the input method's main routine.  The composed text is stored
82      * in buffer.
83      */
dispatchEvent(AWTEvent event)84     public void dispatchEvent(AWTEvent event) {
85         // This input method handles KeyEvent only.
86         if (!(event instanceof KeyEvent)) {
87             return;
88         }
89 
90         KeyEvent e = (KeyEvent) event;
91         int eventID = event.getID();
92         boolean notInCompositionMode = buffer.length() == 0;
93 
94         if (eventID == KeyEvent.KEY_PRESSED) {
95             // If we are not in composition mode, pass through
96             if (notInCompositionMode) {
97                 return;
98             }
99 
100             switch (e.getKeyCode()) {
101                 case KeyEvent.VK_LEFT:
102                     moveCaretLeft();
103                     break;
104                 case KeyEvent.VK_RIGHT:
105                     moveCaretRight();
106                     break;
107             }
108         } else if (eventID == KeyEvent.KEY_TYPED) {
109             char c = e.getKeyChar();
110 
111             // If we are not in composition mode, wait a back slash
112             if (notInCompositionMode) {
113                 // If the type character is not a back slash, pass through
114                 if (c != '\\') {
115                     return;
116                 }
117 
118                 startComposition();     // Enter to composition mode
119             } else {
120                 switch (c) {
121                     case ' ':       // Exit from composition mode
122                         finishComposition();
123                         break;
124                     case '\u007f':  // Delete
125                         deleteCharacter();
126                         break;
127                     case '\b':      // BackSpace
128                         deletePreviousCharacter();
129                         break;
130                     case '\u001b':  // Escape
131                         cancelComposition();
132                         break;
133                     case '\n':      // Return
134                     case '\t':      // Tab
135                         sendCommittedText();
136                         break;
137                     default:
138                         composeUnicodeEscape(c);
139                         break;
140                 }
141             }
142         } else {  // KeyEvent.KEY_RELEASED
143             // If we are not in composition mode, pass through
144             if (notInCompositionMode) {
145                 return;
146             }
147         }
148 
149         e.consume();
150     }
151 
composeUnicodeEscape(char c)152     private void composeUnicodeEscape(char c) {
153         switch (buffer.length()) {
154             case 1:  // \\
155                 waitEscapeCharacter(c);
156                 break;
157             case 2:  // \\u or \\U
158             case 3:  // \\ux or \\Ux
159             case 4:  // \\uxx or \\Uxx
160                 waitDigit(c);
161                 break;
162             case 5:  // \\uxxx or \\Uxxx
163                 if (format == SPECIAL_ESCAPE) {
164                     waitDigit(c);
165                 } else {
166                     waitDigit2(c);
167                 }
168                 break;
169             case 6:  // \\uxxxx or \\Uxxxx
170                 if (format == SPECIAL_ESCAPE) {
171                     waitDigit(c);
172                 } else if (format == SURROGATE_PAIR) {
173                     waitBackSlashOrLowSurrogate(c);
174                 } else {
175                     beep();
176                 }
177                 break;
178             case 7:  // \\Uxxxxx
179                 // Only SPECIAL_ESCAPE format uses this state.
180                 // Since the second "\\u" of SURROGATE_PAIR format is inserted
181                 // automatically, users don't have to type these keys.
182                 waitDigit(c);
183                 break;
184             case 8:  // \\uxxxx\\u
185             case 9:  // \\uxxxx\\ux
186             case 10: // \\uxxxx\\uxx
187             case 11: // \\uxxxx\\uxxx
188                 if (format == SURROGATE_PAIR) {
189                     waitDigit(c);
190                 } else {
191                     beep();
192                 }
193                 break;
194             default:
195                 beep();
196                 break;
197         }
198     }
199 
waitEscapeCharacter(char c)200     private void waitEscapeCharacter(char c) {
201         if (c == 'u' || c == 'U') {
202             buffer.append(c);
203             insertionPoint++;
204             sendComposedText();
205             format = (c == 'u') ? ESCAPE : SPECIAL_ESCAPE;
206         } else {
207             if (c != '\\') {
208                 buffer.append(c);
209                 insertionPoint++;
210             }
211             sendCommittedText();
212         }
213     }
214 
waitDigit(char c)215     private void waitDigit(char c) {
216         if (Character.digit(c, 16) != -1) {
217             buffer.insert(insertionPoint++, c);
218             sendComposedText();
219         } else {
220             beep();
221         }
222     }
223 
waitDigit2(char c)224     private void waitDigit2(char c) {
225         if (Character.digit(c, 16) != -1) {
226             buffer.insert(insertionPoint++, c);
227             char codePoint = (char) getCodePoint(buffer, 2, 5);
228             if (Character.isHighSurrogate(codePoint)) {
229                 format = SURROGATE_PAIR;
230                 buffer.append("\\u");
231                 insertionPoint = 8;
232             } else {
233                 format = ESCAPE;
234             }
235             sendComposedText();
236         } else {
237             beep();
238         }
239     }
240 
waitBackSlashOrLowSurrogate(char c)241     private void waitBackSlashOrLowSurrogate(char c) {
242         if (insertionPoint == 6) {
243             if (c == '\\') {
244                 buffer.append(c);
245                 buffer.append('u');
246                 insertionPoint = 8;
247                 sendComposedText();
248             } else if (Character.digit(c, 16) != -1) {
249                 buffer.append("\\u");
250                 buffer.append(c);
251                 insertionPoint = 9;
252                 sendComposedText();
253             } else {
254                 beep();
255             }
256         } else {
257             beep();
258         }
259     }
260 
261     /**
262      * Send the composed text to the client.
263      */
sendComposedText()264     private void sendComposedText() {
265         AttributedString as = new AttributedString(buffer.toString());
266         as.addAttribute(TextAttribute.INPUT_METHOD_HIGHLIGHT,
267                 InputMethodHighlight.SELECTED_RAW_TEXT_HIGHLIGHT);
268         context.dispatchInputMethodEvent(
269                 InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
270                 as.getIterator(), 0,
271                 TextHitInfo.leading(insertionPoint), null);
272     }
273 
274     /**
275      * Send the committed text to the client.
276      */
sendCommittedText()277     private void sendCommittedText() {
278         AttributedString as = new AttributedString(buffer.toString());
279         context.dispatchInputMethodEvent(
280                 InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
281                 as.getIterator(), buffer.length(),
282                 TextHitInfo.leading(insertionPoint), null);
283 
284         buffer.setLength(0);
285         insertionPoint = 0;
286         format = UNSET;
287     }
288 
289     /**
290      * Move the insertion point one position to the left in the composed text.
291      * Do not let the caret move to the left of the "\\u" or "\\U".
292      */
moveCaretLeft()293     private void moveCaretLeft() {
294         int len = buffer.length();
295         if (--insertionPoint < 2) {
296             insertionPoint++;
297             beep();
298         } else if (format == SURROGATE_PAIR && insertionPoint == 7) {
299             insertionPoint = 8;
300             beep();
301         }
302 
303         context.dispatchInputMethodEvent(
304                 InputMethodEvent.CARET_POSITION_CHANGED,
305                 null, 0,
306                 TextHitInfo.leading(insertionPoint), null);
307     }
308 
309     /**
310      * Move the insertion point one position to the right in the composed text.
311      */
moveCaretRight()312     private void moveCaretRight() {
313         int len = buffer.length();
314         if (++insertionPoint > len) {
315             insertionPoint = len;
316             beep();
317         }
318 
319         context.dispatchInputMethodEvent(
320                 InputMethodEvent.CARET_POSITION_CHANGED,
321                 null, 0,
322                 TextHitInfo.leading(insertionPoint), null);
323     }
324 
325     /**
326      * Delete the character preceding the insertion point in the composed text.
327      * If the insertion point is not at the end of the composed text and the
328      * preceding text is "\\u" or "\\U", ring the bell.
329      */
deletePreviousCharacter()330     private void deletePreviousCharacter() {
331         if (insertionPoint == 2) {
332             if (buffer.length() == 2) {
333                 cancelComposition();
334             } else {
335                 // Do not allow deletion of the leading "\\u" or "\\U" if there
336                 // are other digits in the composed text.
337                 beep();
338             }
339         } else if (insertionPoint == 8) {
340             if (buffer.length() == 8) {
341                 if (format == SURROGATE_PAIR) {
342                     buffer.deleteCharAt(--insertionPoint);
343                 }
344                 buffer.deleteCharAt(--insertionPoint);
345                 sendComposedText();
346             } else {
347                 // Do not allow deletion of the second "\\u" if there are other
348                 // digits in the composed text.
349                 beep();
350             }
351         } else {
352             buffer.deleteCharAt(--insertionPoint);
353             if (buffer.length() == 0) {
354                 sendCommittedText();
355             } else {
356                 sendComposedText();
357             }
358         }
359     }
360 
361     /**
362      * Delete the character following the insertion point in the composed text.
363      * If the insertion point is at the end of the composed text, ring the bell.
364      */
deleteCharacter()365     private void deleteCharacter() {
366         if (insertionPoint < buffer.length()) {
367             buffer.deleteCharAt(insertionPoint);
368             sendComposedText();
369         } else {
370             beep();
371         }
372     }
373 
startComposition()374     private void startComposition() {
375         buffer.append('\\');
376         insertionPoint = 1;
377         sendComposedText();
378     }
379 
cancelComposition()380     private void cancelComposition() {
381         buffer.setLength(0);
382         insertionPoint = 0;
383         sendCommittedText();
384     }
385 
finishComposition()386     private void finishComposition() {
387         int len = buffer.length();
388         if (len == 6 && format != SPECIAL_ESCAPE) {
389             char codePoint = (char) getCodePoint(buffer, 2, 5);
390             if (Character.isValidCodePoint(codePoint) && codePoint != 0xFFFF) {
391                 buffer.setLength(0);
392                 buffer.append(codePoint);
393                 sendCommittedText();
394                 return;
395             }
396         } else if (len == 8 && format == SPECIAL_ESCAPE) {
397             int codePoint = getCodePoint(buffer, 2, 7);
398             if (Character.isValidCodePoint(codePoint) && codePoint != 0xFFFF) {
399                 buffer.setLength(0);
400                 buffer.appendCodePoint(codePoint);
401                 sendCommittedText();
402                 return;
403             }
404         } else if (len == 12 && format == SURROGATE_PAIR) {
405             char[] codePoint = {
406                 (char) getCodePoint(buffer, 2, 5),
407                 (char) getCodePoint(buffer, 8, 11)
408             };
409             if (Character.isHighSurrogate(codePoint[0]) && Character.
410                     isLowSurrogate(codePoint[1])) {
411                 buffer.setLength(0);
412                 buffer.append(codePoint);
413                 sendCommittedText();
414                 return;
415             }
416         }
417 
418         beep();
419     }
420 
getCodePoint(StringBuffer sb, int from, int to)421     private int getCodePoint(StringBuffer sb, int from, int to) {
422         int value = 0;
423         for (int i = from; i <= to; i++) {
424             value = (value << 4) + Character.digit(sb.charAt(i), 16);
425         }
426         return value;
427     }
428 
beep()429     private static void beep() {
430         Toolkit.getDefaultToolkit().beep();
431     }
432 
activate()433     public void activate() {
434         if (buffer == null) {
435             buffer = new StringBuffer(12);
436             insertionPoint = 0;
437         }
438     }
439 
deactivate(boolean isTemporary)440     public void deactivate(boolean isTemporary) {
441         if (!isTemporary) {
442             buffer = null;
443         }
444     }
445 
dispose()446     public void dispose() {
447     }
448 
getControlObject()449     public Object getControlObject() {
450         return null;
451     }
452 
endComposition()453     public void endComposition() {
454         sendCommittedText();
455     }
456 
getLocale()457     public Locale getLocale() {
458         return locale;
459     }
460 
hideWindows()461     public void hideWindows() {
462     }
463 
isCompositionEnabled()464     public boolean isCompositionEnabled() {
465         // always enabled
466         return true;
467     }
468 
notifyClientWindowChange(Rectangle location)469     public void notifyClientWindowChange(Rectangle location) {
470     }
471 
reconvert()472     public void reconvert() {
473         // not supported yet
474         throw new UnsupportedOperationException();
475     }
476 
removeNotify()477     public void removeNotify() {
478     }
479 
setCharacterSubsets(Character.Subset[] subsets)480     public void setCharacterSubsets(Character.Subset[] subsets) {
481     }
482 
setCompositionEnabled(boolean enable)483     public void setCompositionEnabled(boolean enable) {
484         // not supported yet
485         throw new UnsupportedOperationException();
486     }
487 
setInputMethodContext(InputMethodContext context)488     public void setInputMethodContext(InputMethodContext context) {
489         this.context = context;
490     }
491 
492     /*
493      * The Code Point Input Method supports all locales.
494      */
setLocale(Locale locale)495     public boolean setLocale(Locale locale) {
496         this.locale = locale;
497         return true;
498     }
499 }
500