2 *******************************************************************************
3 * Copyright (C) 2004-2011, International Business Machines Corporation and *
4 * others. All Rights Reserved. *
5 *******************************************************************************
8 package com.ibm.icu.dev.tool.ime.translit;
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;
38 import javax.swing.JComboBox;
39 import javax.swing.JLabel;
40 import javax.swing.JList;
41 import javax.swing.ListCellRenderer;
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;
48 public class TransliteratorInputMethod implements InputMethod {
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");
54 return os.indexOf("Windows") == -1;
59 // true if Solaris style; false if PC style, assume Apple uses PC style for now
60 private static final boolean attachedStatusWindow = usesAttachedIME();
62 // the shared status window
63 private static Window statusWindow;
65 // current or last owner
66 private static TransliteratorInputMethod statusWindowOwner;
68 // cache location limits for attached
69 private static Rectangle attachedLimits;
71 // convenience of access, to reflect the current state
72 private static JComboBox choices;
78 // if we're attached, the status window follows the client window
79 private Point attachedLocation;
81 private static int gid;
83 private int id = gid++;
85 InputMethodContext imc;
86 private boolean enabled = true;
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;
96 private static boolean TRACE_EVENT = false;
97 private static boolean TRACE_MESSAGES = false;
98 private static boolean TRACE_BUFFER = false;
100 public TransliteratorInputMethod() {
102 dumpStatus("<constructor>");
104 buffer = new StringBuffer();
105 replaceableText = new ReplaceableString(buffer);
106 index = new Transliterator.Position();
109 public void dumpStatus(String msg) {
110 System.out.println("(" + this + ") " + msg);
113 public void setInputMethodContext(InputMethodContext context) {
114 initStatusWindow(context);
117 imc.enableClientWindowNotification(this, attachedStatusWindow);
120 private static void initStatusWindow(InputMethodContext context) {
121 if (statusWindow == null) {
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";
132 Window sw = context.createInputMethodWindow(title, false);
134 // get all the ICU Transliterators
135 Enumeration en = Transliterator.getAvailableIDs();
136 TreeSet types = new TreeSet(new LabelComparator());
138 while (en.hasMoreElements()) {
139 String id = (String) en.nextElement();
140 String name = Transliterator.getDisplayName(id);
141 JLabel label = new JLabel(name);
146 // add the transliterators to the combo box
148 choices = new JComboBox(types.toArray());
150 choices.setEditable(false);
151 choices.setSelectedIndex(0);
152 choices.setRenderer(new NameRenderer());
153 choices.setActionCommand("transliterator");
155 choices.addActionListener(new ActionListener() {
156 public void actionPerformed(ActionEvent e) {
157 if (statusWindowOwner != null) {
158 statusWindowOwner.statusWindowAction(e);
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);
172 sw.setLocation(sd.width - wd.width, sd.height - wd.height - 25);
175 synchronized (TransliteratorInputMethod.class) {
176 if (statusWindow == null) {
183 private void statusWindowAction(ActionEvent e) {
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
190 dumpStatus("status window action oldIndex: " + selectedIndex
191 + " newIndex: " + si);
195 JLabel item = (JLabel) cb.getSelectedItem();
197 // construct the actual transliterator
198 // commit any text that may be present first
201 transliterator = Transliterator.getInstance(item.getName());
202 desiredContext = transliterator.getMaximumContextLength();
207 dumpStatus("<<status window action");
210 // java has no pin to rectangle function?
211 private static void pin(Point p, Rectangle r) {
214 } else if (p.x > r.x + r.width) {
219 } else if (p.y > r.y + r.height) {
220 p.y = r.y + r.height;
224 public void notifyClientWindowChange(Rectangle location) {
226 dumpStatus(">>notify client window change: " + location);
227 synchronized (TransliteratorInputMethod.class) {
228 if (statusWindowOwner == this) {
229 if (location == null) {
230 statusWindow.setVisible(false);
232 attachedLocation = new Point(location.x, location.y
234 pin(attachedLocation, attachedLimits);
235 statusWindow.setLocation(attachedLocation);
236 statusWindow.setVisible(true);
241 dumpStatus("<<notify client window change: " + location);
244 public void activate() {
246 dumpStatus(">>activate");
248 synchronized (TransliteratorInputMethod.class) {
249 if (statusWindowOwner != this) {
251 dumpStatus("setStatusWindowOwner from: " + statusWindowOwner + " to: " + this);
253 statusWindowOwner = this;
254 // will be null before first change notification
255 if (attachedStatusWindow && attachedLocation != null) {
256 statusWindow.setLocation(attachedLocation);
258 choices.setSelectedIndex(selectedIndex == -1 ? choices
259 .getSelectedIndex() : selectedIndex);
262 choices.setForeground(Color.BLACK);
263 statusWindow.setVisible(true);
266 dumpStatus("<<activate");
269 public void deactivate(boolean isTemporary) {
271 dumpStatus(">>deactivate" + (isTemporary ? " (temporary)" : ""));
273 synchronized (TransliteratorInputMethod.class) {
274 choices.setForeground(Color.LIGHT_GRAY);
278 dumpStatus("<<deactivate" + (isTemporary ? " (temporary)" : ""));
281 public void hideWindows() {
283 dumpStatus(">>hideWindows");
284 synchronized (TransliteratorInputMethod.class) {
285 if (statusWindowOwner == this) {
287 dumpStatus("hiding");
288 statusWindow.setVisible(false);
292 dumpStatus("<<hideWindows");
295 public boolean setLocale(Locale locale) {
299 public Locale getLocale() {
300 return Locale.getDefault();
303 public void setCharacterSubsets(Character.Subset[] subsets) {
306 public void reconvert() {
307 throw new UnsupportedOperationException();
310 public void removeNotify() {
312 dumpStatus("**removeNotify");
315 public void endComposition() {
319 public void dispose() {
321 dumpStatus("**dispose");
324 public Object getControlObject() {
328 public void setCompositionEnabled(boolean enable) {
332 public boolean isCompositionEnabled() {
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));
344 index1 = info.indexOf("] on ");
345 index2 = info.indexOf("[", index1);
347 int index3 = info.lastIndexOf(".", index2);
348 if (index3 < index1 + 4) {
352 buf.append(info.substring(index3 + 1, index2));
354 return buf.toString();
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;
364 switch (event.getID()) {
365 case MouseEvent.MOUSE_PRESSED:
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
373 case KeyEvent.KEY_TYPED: {
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
380 if (handleTyped(ke.getKeyChar())) {
387 case KeyEvent.KEY_PRESSED: {
389 KeyEvent ke = (KeyEvent)event;
390 if (TRACE_EVENT) System.out.println("TIM: " + eventInfo(ke));
391 if (handlePressed(ke.getKeyCode())) {
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);
411 private void reset() {
412 buffer.delete(0, buffer.length());
413 index.contextStart = index.contextLimit = index.start = index.limit = 0;
416 // committed}context-composed|composed
420 private void traceBuffer(String msg, int cc, int off) {
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) + '\''));
429 private void update(boolean flush) {
430 int len = buffer.length();
431 String text = buffer.toString();
432 AttributedString as = new AttributedString(text);
436 off = index.contextLimit - len; // will be negative
437 cc = index.start = index.limit = index.contextLimit = len;
439 cc = index.start > desiredContext ? index.start - desiredContext
441 off = index.contextLimit - cc;
444 if (index.start < len) {
445 as.addAttribute(TextAttribute.INPUT_METHOD_HIGHLIGHT,
446 InputMethodHighlight.SELECTED_RAW_TEXT_HIGHLIGHT,
450 imc.dispatchInputMethodEvent(
451 InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, as.getIterator(),
452 cc, TextHitInfo.leading(off), null);
454 traceBuffer("update", cc, off);
457 buffer.delete(0, cc);
460 index.contextLimit -= cc;
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);
470 private void caretToStart() {
471 if (index.contextLimit > index.start) {
472 index.contextLimit = index.limit = index.start;
477 private void caretToLimit() {
478 if (index.contextLimit < buffer.length()) {
479 index.contextLimit = index.limit = buffer.length();
484 private boolean caretTowardsStart() {
485 int bufpos = index.contextLimit;
486 if (bufpos > index.start) {
488 if (bufpos > index.start
489 && UCharacter.isLowSurrogate(buffer.charAt(bufpos))
490 && UCharacter.isHighSurrogate(buffer.charAt(bufpos - 1))) {
493 index.contextLimit = index.limit = bufpos;
500 private boolean caretTowardsLimit() {
501 int bufpos = index.contextLimit;
502 if (bufpos < buffer.length()) {
504 if (bufpos < buffer.length()
505 && UCharacter.isLowSurrogate(buffer.charAt(bufpos))
506 && UCharacter.isHighSurrogate(buffer.charAt(bufpos - 1))) {
509 index.contextLimit = index.limit = bufpos;
516 private boolean canBackspace() {
517 return index.contextLimit > 0;
520 private boolean backspace() {
521 int bufpos = index.contextLimit;
525 if (bufpos > 0 && UCharacter.isLowSurrogate(buffer.charAt(bufpos))
526 && UCharacter.isHighSurrogate(buffer.charAt(bufpos - 1))) {
529 if (bufpos < index.start) {
530 index.start = bufpos;
532 index.contextLimit = index.limit = bufpos;
533 doDelete(bufpos, limit);
539 private boolean canDelete() {
540 return index.contextLimit < buffer.length();
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))) {
552 doDelete(bufpos, limit);
558 private void doDelete(int start, int limit) {
559 buffer.delete(start, limit);
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);
570 index.start = index.limit = index.contextLimit = 0;
578 private void clearAll() {
579 int len = buffer.length();
581 if (len > index.start) {
582 buffer.delete(index.start, len);
588 private boolean insert(char c) {
589 transliterator.transliterate(replaceableText, index, c);
594 private boolean editing() {
595 return buffer.length() > 0;
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
606 * @return whether the character was handled
608 private boolean handleTyped(char 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);
622 * Handle keyPressed events.
624 private boolean handlePressed(int code) {
625 if (enabled && editing()) {
627 case KeyEvent.VK_PAGE_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
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
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.
664 public String toString() {
665 final String[] names = {
666 "alice", "bill", "carrie", "doug", "elena", "frank", "gertie", "howie", "ingrid", "john"
669 if (id < names.length) {
672 return names[id] + "-" + (id/names.length);
677 class NameRenderer extends JLabel implements ListCellRenderer {
682 private static final long serialVersionUID = -210152863798631747L;
684 public Component getListCellRendererComponent(
689 boolean cellHasFocus) {
691 String s = ((JLabel)value).getText();
695 setBackground(list.getSelectionBackground());
696 setForeground(list.getSelectionForeground());
698 setBackground(list.getBackground());
699 setForeground(list.getForeground());
702 setEnabled(list.isEnabled());
703 setFont(list.getFont());
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());
715 public boolean equals(Object obj1) {
716 return obj1 instanceof LabelComparator;