• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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) 2004-2011, International Business Machines Corporation and    *
6  * others. All Rights Reserved.                                                *
7  *******************************************************************************
8  */
9 
10 package com.ibm.icu.dev.tool.ime.translit;
11 
12 import java.awt.AWTEvent;
13 import java.awt.Color;
14 import java.awt.Component;
15 import java.awt.Dimension;
16 import java.awt.Point;
17 import java.awt.Rectangle;
18 import java.awt.Toolkit;
19 import java.awt.Window;
20 import java.awt.event.ActionEvent;
21 import java.awt.event.ActionListener;
22 import java.awt.event.InputEvent;
23 import java.awt.event.InputMethodEvent;
24 import java.awt.event.KeyEvent;
25 import java.awt.event.MouseEvent;
26 import java.awt.font.TextAttribute;
27 import java.awt.font.TextHitInfo;
28 import java.awt.im.InputMethodHighlight;
29 import java.awt.im.spi.InputMethod;
30 import java.awt.im.spi.InputMethodContext;
31 import java.text.AttributedString;
32 import java.text.Collator;
33 import java.util.Comparator;
34 import java.util.Enumeration;
35 import java.util.Locale;
36 import java.util.MissingResourceException;
37 import java.util.ResourceBundle;
38 import java.util.TreeSet;
39 
40 import javax.swing.JComboBox;
41 import javax.swing.JLabel;
42 import javax.swing.JList;
43 import javax.swing.ListCellRenderer;
44 
45 import com.ibm.icu.impl.Utility;
46 import com.ibm.icu.lang.UCharacter;
47 import com.ibm.icu.text.ReplaceableString;
48 import com.ibm.icu.text.Transliterator;
49 
50 public class TransliteratorInputMethod implements InputMethod {
51 
usesAttachedIME()52     private static boolean usesAttachedIME() {
53         // we're in the ext directory so permissions are not an issue
54         String os = System.getProperty("os.name");
55         if (os != null) {
56             return os.indexOf("Windows") == -1;
57         }
58         return false;
59     }
60 
61     // true if Solaris style; false if PC style, assume Apple uses PC style for now
62     private static final boolean attachedStatusWindow = usesAttachedIME();
63 
64     // the shared status window
65     private static Window statusWindow;
66 
67     // current or last owner
68     private static TransliteratorInputMethod statusWindowOwner;
69 
70     // cache location limits for attached
71     private static Rectangle attachedLimits;
72 
73     // convenience of access, to reflect the current state
74     private static JComboBox choices;
75 
76     //
77     // per-instance state
78     //
79 
80     // if we're attached, the status window follows the client window
81     private Point attachedLocation;
82 
83     private static int gid;
84 
85     private int id = gid++;
86 
87     InputMethodContext imc;
88     private boolean enabled = true;
89 
90     private int selectedIndex = -1; // index in JComboBox corresponding to our transliterator
91     private Transliterator transliterator;
92     private int desiredContext;
93     private StringBuffer buffer;
94     private ReplaceableString replaceableText;
95     private Transliterator.Position index;
96 
97     // debugging
98     private static boolean TRACE_EVENT = false;
99     private static boolean TRACE_MESSAGES = false;
100     private static boolean TRACE_BUFFER = false;
101 
TransliteratorInputMethod()102     public TransliteratorInputMethod() {
103         if (TRACE_MESSAGES)
104             dumpStatus("<constructor>");
105 
106         buffer = new StringBuffer();
107         replaceableText = new ReplaceableString(buffer);
108         index = new Transliterator.Position();
109     }
110 
dumpStatus(String msg)111     public void dumpStatus(String msg) {
112         System.out.println("(" + this + ") " + msg);
113     }
114 
setInputMethodContext(InputMethodContext context)115     public void setInputMethodContext(InputMethodContext context) {
116         initStatusWindow(context);
117 
118         imc = context;
119         imc.enableClientWindowNotification(this, attachedStatusWindow);
120     }
121 
initStatusWindow(InputMethodContext context)122     private static void initStatusWindow(InputMethodContext context) {
123         if (statusWindow == null) {
124             String title;
125             try {
126                 ResourceBundle rb = ResourceBundle
127                         .getBundle("com.ibm.icu.dev.tool.ime.translit.Transliterator");
128                 title = rb.getString("title");
129             } catch (MissingResourceException m) {
130                 System.out.println("Transliterator resources missing: " + m);
131                 title = "Transliterator Input Method";
132             }
133 
134             Window sw = context.createInputMethodWindow(title, false);
135 
136             // get all the ICU Transliterators
137             Enumeration en = Transliterator.getAvailableIDs();
138             TreeSet types = new TreeSet(new LabelComparator());
139 
140             while (en.hasMoreElements()) {
141                 String id = (String) en.nextElement();
142                 String name = Transliterator.getDisplayName(id);
143                 JLabel label = new JLabel(name);
144                 label.setName(id);
145                 types.add(label);
146             }
147 
148             // add the transliterators to the combo box
149 
150             choices = new JComboBox(types.toArray());
151 
152             choices.setEditable(false);
153             choices.setSelectedIndex(0);
154             choices.setRenderer(new NameRenderer());
155             choices.setActionCommand("transliterator");
156 
157             choices.addActionListener(new ActionListener() {
158                 public void actionPerformed(ActionEvent e) {
159                     if (statusWindowOwner != null) {
160                         statusWindowOwner.statusWindowAction(e);
161                     }
162                 }
163             });
164 
165             sw.add(choices);
166             sw.pack();
167 
168             Dimension sd = Toolkit.getDefaultToolkit().getScreenSize();
169             Dimension wd = sw.getSize();
170             if (attachedStatusWindow) {
171                 attachedLimits = new Rectangle(0, 0, sd.width - wd.width,
172                         sd.height - wd.height);
173             } else {
174                 sw.setLocation(sd.width - wd.width, sd.height - wd.height - 25);
175             }
176 
177             synchronized (TransliteratorInputMethod.class) {
178                 if (statusWindow == null) {
179                     statusWindow = sw;
180                 }
181             }
182         }
183     }
184 
statusWindowAction(ActionEvent e)185     private void statusWindowAction(ActionEvent e) {
186         if (TRACE_MESSAGES)
187             dumpStatus(">>status window action");
188         JComboBox cb = (JComboBox) e.getSource();
189         int si = cb.getSelectedIndex();
190         if (si != selectedIndex) { // otherwise, we don't need to change
191             if (TRACE_MESSAGES)
192                 dumpStatus("status window action oldIndex: " + selectedIndex
193                         + " newIndex: " + si);
194 
195             selectedIndex = si;
196 
197             JLabel item = (JLabel) cb.getSelectedItem();
198 
199             // construct the actual transliterator
200             // commit any text that may be present first
201             commitAll();
202 
203             transliterator = Transliterator.getInstance(item.getName());
204             desiredContext = transliterator.getMaximumContextLength();
205 
206             reset();
207         }
208         if (TRACE_MESSAGES)
209             dumpStatus("<<status window action");
210     }
211 
212     // java has no pin to rectangle function?
pin(Point p, Rectangle r)213     private static void pin(Point p, Rectangle r) {
214         if (p.x < r.x) {
215             p.x = r.x;
216         } else if (p.x > r.x + r.width) {
217             p.x = r.x + r.width;
218         }
219         if (p.y < r.y) {
220             p.y = r.y;
221         } else if (p.y > r.y + r.height) {
222             p.y = r.y + r.height;
223         }
224     }
225 
notifyClientWindowChange(Rectangle location)226     public void notifyClientWindowChange(Rectangle location) {
227         if (TRACE_MESSAGES)
228             dumpStatus(">>notify client window change: " + location);
229         synchronized (TransliteratorInputMethod.class) {
230             if (statusWindowOwner == this) {
231                 if (location == null) {
232                     statusWindow.setVisible(false);
233                 } else {
234                     attachedLocation = new Point(location.x, location.y
235                             + location.height);
236                     pin(attachedLocation, attachedLimits);
237                     statusWindow.setLocation(attachedLocation);
238                     statusWindow.setVisible(true);
239                 }
240             }
241         }
242         if (TRACE_MESSAGES)
243             dumpStatus("<<notify client window change: " + location);
244     }
245 
activate()246     public void activate() {
247         if (TRACE_MESSAGES)
248             dumpStatus(">>activate");
249 
250         synchronized (TransliteratorInputMethod.class) {
251             if (statusWindowOwner != this) {
252                 if (TRACE_MESSAGES)
253                     dumpStatus("setStatusWindowOwner from: " + statusWindowOwner + " to: " + this);
254 
255                 statusWindowOwner = this;
256                 // will be null before first change notification
257                 if (attachedStatusWindow && attachedLocation != null) {
258                     statusWindow.setLocation(attachedLocation);
259                 }
260                 choices.setSelectedIndex(selectedIndex == -1 ? choices
261                         .getSelectedIndex() : selectedIndex);
262             }
263 
264             choices.setForeground(Color.BLACK);
265             statusWindow.setVisible(true);
266         }
267         if (TRACE_MESSAGES)
268             dumpStatus("<<activate");
269     }
270 
deactivate(boolean isTemporary)271     public void deactivate(boolean isTemporary) {
272         if (TRACE_MESSAGES)
273             dumpStatus(">>deactivate" + (isTemporary ? " (temporary)" : ""));
274         if (!isTemporary) {
275             synchronized (TransliteratorInputMethod.class) {
276                 choices.setForeground(Color.LIGHT_GRAY);
277             }
278         }
279         if (TRACE_MESSAGES)
280             dumpStatus("<<deactivate" + (isTemporary ? " (temporary)" : ""));
281     }
282 
hideWindows()283     public void hideWindows() {
284         if (TRACE_MESSAGES)
285             dumpStatus(">>hideWindows");
286         synchronized (TransliteratorInputMethod.class) {
287             if (statusWindowOwner == this) {
288                 if (TRACE_MESSAGES)
289                     dumpStatus("hiding");
290                 statusWindow.setVisible(false);
291             }
292         }
293         if (TRACE_MESSAGES)
294             dumpStatus("<<hideWindows");
295     }
296 
setLocale(Locale locale)297     public boolean setLocale(Locale locale) {
298         return false;
299     }
300 
getLocale()301     public Locale getLocale() {
302         return Locale.getDefault();
303     }
304 
setCharacterSubsets(Character.Subset[] subsets)305     public void setCharacterSubsets(Character.Subset[] subsets) {
306     }
307 
reconvert()308     public void reconvert() {
309         throw new UnsupportedOperationException();
310     }
311 
removeNotify()312     public void removeNotify() {
313         if (TRACE_MESSAGES)
314             dumpStatus("**removeNotify");
315     }
316 
endComposition()317     public void endComposition() {
318         commitAll();
319     }
320 
dispose()321     public void dispose() {
322         if (TRACE_MESSAGES)
323             dumpStatus("**dispose");
324     }
325 
getControlObject()326     public Object getControlObject() {
327         return null;
328     }
329 
setCompositionEnabled(boolean enable)330     public void setCompositionEnabled(boolean enable) {
331         enabled = enable;
332     }
333 
isCompositionEnabled()334     public boolean isCompositionEnabled() {
335         return enabled;
336     }
337 
338     // debugging
eventInfo(AWTEvent event)339     private String eventInfo(AWTEvent event) {
340         String info = event.toString();
341         StringBuffer buf = new StringBuffer();
342         int index1 = info.indexOf("[");
343         int index2 = info.indexOf(",", index1);
344         buf.append(info.substring(index1 + 1, index2));
345 
346         index1 = info.indexOf("] on ");
347         index2 = info.indexOf("[", index1);
348         if (index2 != -1) {
349             int index3 = info.lastIndexOf(".", index2);
350             if (index3 < index1 + 4) {
351                 index3 = index1 + 4;
352             }
353             buf.append(" on ");
354             buf.append(info.substring(index3 + 1, index2));
355         }
356         return buf.toString();
357     }
358 
dispatchEvent(AWTEvent event)359     public void dispatchEvent(AWTEvent event) {
360         final int MODIFIERS =
361             InputEvent.CTRL_MASK |
362             InputEvent.META_MASK |
363             InputEvent.ALT_MASK |
364             InputEvent.ALT_GRAPH_MASK;
365 
366         switch (event.getID()) {
367         case MouseEvent.MOUSE_PRESSED:
368             if (enabled) {
369                 if (TRACE_EVENT) System.out.println("TIM: " + eventInfo(event));
370                 // we'll get this even if the user is scrolling, can we rely on the component?
371                 // commitAll(); // don't allow even clicks within our own edit area
372             }
373             break;
374 
375         case KeyEvent.KEY_TYPED: {
376             if (enabled) {
377                 KeyEvent ke = (KeyEvent)event;
378                 if (TRACE_EVENT) System.out.println("TIM: " + eventInfo(ke));
379                 if ((ke.getModifiers() & MODIFIERS) != 0) {
380                     commitAll(); // assume a command, let it go through
381                 } else {
382                     if (handleTyped(ke.getKeyChar())) {
383                         ke.consume();
384                     }
385                 }
386             }
387         } break;
388 
389         case KeyEvent.KEY_PRESSED: {
390             if (enabled) {
391             KeyEvent ke = (KeyEvent)event;
392             if (TRACE_EVENT) System.out.println("TIM: " + eventInfo(ke));
393                 if (handlePressed(ke.getKeyCode())) {
394                     ke.consume();
395                 }
396             }
397         } break;
398 
399         case KeyEvent.KEY_RELEASED: {
400             // this won't autorepeat, which is better for toggle actions
401             KeyEvent ke = (KeyEvent)event;
402             if (ke.getKeyCode() == KeyEvent.VK_SPACE && ke.isControlDown()) {
403                 setCompositionEnabled(!enabled);
404             }
405         } break;
406 
407         default:
408             break;
409         }
410     }
411 
412     /** Wipe clean */
reset()413     private void reset() {
414         buffer.delete(0, buffer.length());
415         index.contextStart = index.contextLimit = index.start = index.limit = 0;
416     }
417 
418     // committed}context-composed|composed
419     //          ^       ^        ^
420     //         cc     start    ctxLim
421 
traceBuffer(String msg, int cc, int off)422     private void traceBuffer(String msg, int cc, int off) {
423         if (TRACE_BUFFER)
424             System.out.println(Utility.escape(msg + ": '"
425                     + buffer.substring(0, cc) + '}'
426                     + buffer.substring(cc, index.start) + '-'
427                     + buffer.substring(index.start, index.contextLimit) + '|'
428                     + buffer.substring(index.contextLimit) + '\''));
429     }
430 
update(boolean flush)431     private void update(boolean flush) {
432         int len = buffer.length();
433         String text = buffer.toString();
434         AttributedString as = new AttributedString(text);
435 
436         int cc, off;
437         if (flush) {
438             off = index.contextLimit - len; // will be negative
439             cc = index.start = index.limit = index.contextLimit = len;
440         } else {
441             cc = index.start > desiredContext ? index.start - desiredContext
442                     : 0;
443             off = index.contextLimit - cc;
444         }
445 
446         if (index.start < len) {
447             as.addAttribute(TextAttribute.INPUT_METHOD_HIGHLIGHT,
448                     InputMethodHighlight.SELECTED_RAW_TEXT_HIGHLIGHT,
449                     index.start, len);
450         }
451 
452         imc.dispatchInputMethodEvent(
453                 InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, as.getIterator(),
454                 cc, TextHitInfo.leading(off), null);
455 
456         traceBuffer("update", cc, off);
457 
458         if (cc > 0) {
459             buffer.delete(0, cc);
460             index.start -= cc;
461             index.limit -= cc;
462             index.contextLimit -= cc;
463         }
464     }
465 
updateCaret()466     private void updateCaret() {
467         imc.dispatchInputMethodEvent(InputMethodEvent.CARET_POSITION_CHANGED,
468                 null, 0, TextHitInfo.leading(index.contextLimit), null);
469         traceBuffer("updateCaret", 0, index.contextLimit);
470     }
471 
caretToStart()472     private void caretToStart() {
473         if (index.contextLimit > index.start) {
474             index.contextLimit = index.limit = index.start;
475             updateCaret();
476         }
477     }
478 
caretToLimit()479     private void caretToLimit() {
480         if (index.contextLimit < buffer.length()) {
481             index.contextLimit = index.limit = buffer.length();
482             updateCaret();
483         }
484     }
485 
caretTowardsStart()486     private boolean caretTowardsStart() {
487         int bufpos = index.contextLimit;
488         if (bufpos > index.start) {
489             --bufpos;
490             if (bufpos > index.start
491                     && UCharacter.isLowSurrogate(buffer.charAt(bufpos))
492                     && UCharacter.isHighSurrogate(buffer.charAt(bufpos - 1))) {
493                 --bufpos;
494             }
495             index.contextLimit = index.limit = bufpos;
496             updateCaret();
497             return true;
498         }
499         return commitAll();
500     }
501 
caretTowardsLimit()502     private boolean caretTowardsLimit() {
503         int bufpos = index.contextLimit;
504         if (bufpos < buffer.length()) {
505             ++bufpos;
506             if (bufpos < buffer.length()
507                     && UCharacter.isLowSurrogate(buffer.charAt(bufpos))
508                     && UCharacter.isHighSurrogate(buffer.charAt(bufpos - 1))) {
509                 ++bufpos;
510             }
511             index.contextLimit = index.limit = bufpos;
512             updateCaret();
513             return true;
514         }
515         return commitAll();
516     }
517 
canBackspace()518     private boolean canBackspace() {
519         return index.contextLimit > 0;
520     }
521 
backspace()522     private boolean backspace() {
523         int bufpos = index.contextLimit;
524         if (bufpos > 0) {
525             int limit = bufpos;
526             --bufpos;
527             if (bufpos > 0 && UCharacter.isLowSurrogate(buffer.charAt(bufpos))
528                     && UCharacter.isHighSurrogate(buffer.charAt(bufpos - 1))) {
529                 --bufpos;
530             }
531             if (bufpos < index.start) {
532                 index.start = bufpos;
533             }
534             index.contextLimit = index.limit = bufpos;
535             doDelete(bufpos, limit);
536             return true;
537         }
538         return false;
539     }
540 
canDelete()541     private boolean canDelete() {
542         return index.contextLimit < buffer.length();
543     }
544 
delete()545     private boolean delete() {
546         int bufpos = index.contextLimit;
547         if (bufpos < buffer.length()) {
548             int limit = bufpos + 1;
549             if (limit < buffer.length()
550                     && UCharacter.isHighSurrogate(buffer.charAt(limit - 1))
551                     && UCharacter.isLowSurrogate(buffer.charAt(limit))) {
552                 ++limit;
553             }
554             doDelete(bufpos, limit);
555             return true;
556         }
557         return false;
558     }
559 
doDelete(int start, int limit)560     private void doDelete(int start, int limit) {
561         buffer.delete(start, limit);
562         update(false);
563     }
564 
commitAll()565     private boolean commitAll() {
566         if (buffer.length() > 0) {
567             boolean atStart = index.start == index.contextLimit;
568             boolean didConvert = buffer.length() > index.start;
569             index.contextLimit = index.limit = buffer.length();
570             transliterator.finishTransliteration(replaceableText, index);
571             if (atStart) {
572                 index.start = index.limit = index.contextLimit = 0;
573             }
574             update(true);
575             return didConvert;
576         }
577         return false;
578     }
579 
clearAll()580     private void clearAll() {
581         int len = buffer.length();
582         if (len > 0) {
583             if (len > index.start) {
584                 buffer.delete(index.start, len);
585             }
586             update(true);
587         }
588     }
589 
insert(char c)590     private boolean insert(char c) {
591         transliterator.transliterate(replaceableText, index, c);
592         update(false);
593         return true;
594     }
595 
editing()596     private boolean editing() {
597         return buffer.length() > 0;
598     }
599 
600     /**
601      * The big problem is that from release to release swing changes how it
602      * handles some characters like tab and backspace.  Sometimes it handles
603      * them as keyTyped events, and sometimes it handles them as keyPressed
604      * events.  If you want to allow the event to go through so swing handles
605      * it, you have to allow one or the other to go through.  If you don't want
606      * the event to go through so you can handle it, you have to stop the
607      * event both places.
608      * @return whether the character was handled
609      */
handleTyped(char ch)610     private boolean handleTyped(char ch) {
611         if (enabled) {
612             switch (ch) {
613             case '\b': if (editing()) return backspace(); break;
614             case '\t': if (editing()) { return commitAll(); } break;
615             case '\u001b': if (editing()) { clearAll(); return true; } break;
616             case '\u007f': if (editing()) return delete(); break;
617             default: return insert(ch);
618             }
619         }
620         return false;
621     }
622 
623     /**
624      * Handle keyPressed events.
625      */
handlePressed(int code)626     private boolean handlePressed(int code) {
627         if (enabled && editing()) {
628             switch (code) {
629             case KeyEvent.VK_PAGE_UP:
630             case KeyEvent.VK_UP:
631             case KeyEvent.VK_KP_UP:
632             case KeyEvent.VK_HOME:
633             caretToStart(); return true;
634             case KeyEvent.VK_PAGE_DOWN:
635             case KeyEvent.VK_DOWN:
636             case KeyEvent.VK_KP_DOWN:
637             case KeyEvent.VK_END:
638             caretToLimit(); return true;
639             case KeyEvent.VK_LEFT:
640             case KeyEvent.VK_KP_LEFT:
641             return caretTowardsStart();
642             case KeyEvent.VK_RIGHT:
643             case KeyEvent.VK_KP_RIGHT:
644             return caretTowardsLimit();
645             case KeyEvent.VK_BACK_SPACE:
646             return canBackspace(); // unfortunately, in 1.5 swing handles this in keyPressed instead of keyTyped
647             case KeyEvent.VK_DELETE:
648             return canDelete(); // this too?
649             case KeyEvent.VK_TAB:
650             case KeyEvent.VK_ENTER:
651             return commitAll(); // so we'll never handle VK_TAB in keyTyped
652 
653             case KeyEvent.VK_SHIFT:
654             case KeyEvent.VK_CONTROL:
655             case KeyEvent.VK_ALT:
656             return false; // ignore these unless a key typed event gets generated
657             default:
658             // by default, let editor handle it, and we'll assume that it will tell us
659             // to endComposition if it does anything funky with, e.g., function keys.
660             return false;
661             }
662         }
663         return false;
664     }
665 
toString()666     public String toString() {
667         final String[] names = {
668             "alice", "bill", "carrie", "doug", "elena", "frank", "gertie", "howie", "ingrid", "john"
669         };
670 
671         if (id < names.length) {
672             return names[id];
673         } else {
674             return names[id] + "-" + (id/names.length);
675         }
676     }
677 }
678 
679 class NameRenderer extends JLabel implements ListCellRenderer {
680 
681     /**
682      * For serialization
683      */
684     private static final long serialVersionUID = -210152863798631747L;
685 
getListCellRendererComponent( JList list, Object value, int index, boolean isSelected, boolean cellHasFocus)686     public Component getListCellRendererComponent(
687         JList list,
688         Object value,
689         int index,
690         boolean isSelected,
691         boolean cellHasFocus) {
692 
693         String s = ((JLabel)value).getText();
694         setText(s);
695 
696         if (isSelected) {
697             setBackground(list.getSelectionBackground());
698             setForeground(list.getSelectionForeground());
699         } else {
700             setBackground(list.getBackground());
701             setForeground(list.getForeground());
702         }
703 
704         setEnabled(list.isEnabled());
705         setFont(list.getFont());
706         setOpaque(true);
707         return this;
708     }
709 }
710 
711 class LabelComparator implements Comparator {
compare(Object obj1, Object obj2)712     public int compare(Object obj1, Object obj2) {
713         Collator collator = Collator.getInstance();
714         return collator.compare(((JLabel)obj1).getText(), ((JLabel)obj2).getText());
715     }
716 
equals(Object obj1)717     public boolean equals(Object obj1) {
718         return obj1 instanceof LabelComparator;
719     }
720 }
721