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