2 *******************************************************************************
\r
3 * Copyright (C) 2004-2010, International Business Machines Corporation and *
\r
4 * others. All Rights Reserved. *
\r
5 *******************************************************************************
\r
8 package com.ibm.icu.dev.tool.ime.translit;
\r
10 import java.awt.AWTEvent;
\r
11 import java.awt.Color;
\r
12 import java.awt.Component;
\r
13 import java.awt.Dimension;
\r
14 import java.awt.Point;
\r
15 import java.awt.Rectangle;
\r
16 import java.awt.Toolkit;
\r
17 import java.awt.Window;
\r
18 import java.awt.event.ActionEvent;
\r
19 import java.awt.event.ActionListener;
\r
20 import java.awt.event.InputEvent;
\r
21 import java.awt.event.InputMethodEvent;
\r
22 import java.awt.event.KeyEvent;
\r
23 import java.awt.event.MouseEvent;
\r
24 import java.awt.font.TextAttribute;
\r
25 import java.awt.font.TextHitInfo;
\r
26 import java.awt.im.InputMethodHighlight;
\r
27 import java.awt.im.spi.InputMethod;
\r
28 import java.awt.im.spi.InputMethodContext;
\r
29 import java.text.AttributedString;
\r
30 import java.text.Collator;
\r
31 import java.util.Comparator;
\r
32 import java.util.Enumeration;
\r
33 import java.util.Locale;
\r
34 import java.util.MissingResourceException;
\r
35 import java.util.ResourceBundle;
\r
36 import java.util.TreeSet;
\r
38 import javax.swing.JComboBox;
\r
39 import javax.swing.JLabel;
\r
40 import javax.swing.JList;
\r
41 import javax.swing.ListCellRenderer;
\r
43 import com.ibm.icu.impl.Utility;
\r
44 import com.ibm.icu.lang.UCharacter;
\r
45 import com.ibm.icu.text.ReplaceableString;
\r
46 import com.ibm.icu.text.Transliterator;
\r
48 public class TransliteratorInputMethod implements InputMethod {
\r
50 private static boolean usesAttachedIME() {
\r
51 // we're in the ext directory so permissions are not an issue
\r
52 String os = System.getProperty("os.name");
\r
54 return os.indexOf("Windows") == -1;
\r
59 // true if Solaris style; false if PC style, assume Apple uses PC style for now
\r
60 private static final boolean attachedStatusWindow = usesAttachedIME();
\r
62 // the shared status window
\r
63 private static Window statusWindow;
\r
65 // current or last owner
\r
66 private static TransliteratorInputMethod statusWindowOwner;
\r
68 // cache location limits for attached
\r
69 private static Rectangle attachedLimits;
\r
71 // convenience of access, to reflect the current state
\r
72 private static JComboBox choices;
\r
75 // per-instance state
\r
78 // if we're attached, the status window follows the client window
\r
79 private Point attachedLocation;
\r
81 private static int gid;
\r
83 private int id = gid++;
\r
85 InputMethodContext imc;
\r
86 private boolean enabled = true;
\r
88 private int selectedIndex = -1; // index in JComboBox corresponding to our transliterator
\r
89 private Transliterator transliterator;
\r
90 private int desiredContext;
\r
91 private StringBuffer buffer;
\r
92 private ReplaceableString replaceableText;
\r
93 private Transliterator.Position index;
\r
96 private static boolean TRACE_EVENT = false;
\r
97 private static boolean TRACE_MESSAGES = false;
\r
98 private static boolean TRACE_BUFFER = false;
\r
100 public TransliteratorInputMethod() {
\r
101 if (TRACE_MESSAGES)
\r
102 dumpStatus("<constructor>");
\r
104 buffer = new StringBuffer();
\r
105 replaceableText = new ReplaceableString(buffer);
\r
106 index = new Transliterator.Position();
\r
109 public void dumpStatus(String msg) {
\r
110 System.out.println("(" + this + ") " + msg);
\r
113 public void setInputMethodContext(InputMethodContext context) {
\r
114 initStatusWindow(context);
\r
117 imc.enableClientWindowNotification(this, attachedStatusWindow);
\r
120 private static void initStatusWindow(InputMethodContext context) {
\r
121 if (statusWindow == null) {
\r
124 ResourceBundle rb = ResourceBundle
\r
125 .getBundle("com.ibm.icu.dev.tool.ime.translit.Transliterator");
\r
126 title = rb.getString("title");
\r
127 } catch (MissingResourceException m) {
\r
128 System.out.println("Transliterator resources missing: " + m);
\r
129 title = "Transliterator Input Method";
\r
132 Window sw = context.createInputMethodWindow(title, false);
\r
134 // get all the ICU Transliterators
\r
135 Enumeration en = Transliterator.getAvailableIDs();
\r
136 TreeSet types = new TreeSet(new LabelComparator());
\r
138 while (en.hasMoreElements()) {
\r
139 String id = (String) en.nextElement();
\r
140 String name = Transliterator.getDisplayName(id);
\r
141 JLabel label = new JLabel(name);
\r
146 // add the transliterators to the combo box
\r
148 choices = new JComboBox(types.toArray());
\r
150 choices.setEditable(false);
\r
151 choices.setSelectedIndex(0);
\r
152 choices.setRenderer(new NameRenderer());
\r
153 choices.setActionCommand("transliterator");
\r
155 choices.addActionListener(new ActionListener() {
\r
156 public void actionPerformed(ActionEvent e) {
\r
157 if (statusWindowOwner != null) {
\r
158 statusWindowOwner.statusWindowAction(e);
\r
166 Dimension sd = Toolkit.getDefaultToolkit().getScreenSize();
\r
167 Dimension wd = sw.getSize();
\r
168 if (attachedStatusWindow) {
\r
169 attachedLimits = new Rectangle(0, 0, sd.width - wd.width,
\r
170 sd.height - wd.height);
\r
172 sw.setLocation(sd.width - wd.width, sd.height - wd.height - 25);
\r
175 synchronized (TransliteratorInputMethod.class) {
\r
176 if (statusWindow == null) {
\r
183 private void statusWindowAction(ActionEvent e) {
\r
184 if (TRACE_MESSAGES)
\r
185 dumpStatus(">>status window action");
\r
186 JComboBox cb = (JComboBox) e.getSource();
\r
187 int si = cb.getSelectedIndex();
\r
188 if (si != selectedIndex) { // otherwise, we don't need to change
\r
189 if (TRACE_MESSAGES)
\r
190 dumpStatus("status window action oldIndex: " + selectedIndex
\r
191 + " newIndex: " + si);
\r
193 selectedIndex = si;
\r
195 JLabel item = (JLabel) cb.getSelectedItem();
\r
197 // construct the actual transliterator
\r
198 // commit any text that may be present first
\r
201 transliterator = Transliterator.getInstance(item.getName());
\r
202 desiredContext = transliterator.getMaximumContextLength();
\r
206 if (TRACE_MESSAGES)
\r
207 dumpStatus("<<status window action");
\r
210 // java has no pin to rectangle function?
\r
211 private static void pin(Point p, Rectangle r) {
\r
214 } else if (p.x > r.x + r.width) {
\r
215 p.x = r.x + r.width;
\r
219 } else if (p.y > r.y + r.height) {
\r
220 p.y = r.y + r.height;
\r
224 public void notifyClientWindowChange(Rectangle location) {
\r
225 if (TRACE_MESSAGES)
\r
226 dumpStatus(">>notify client window change: " + location);
\r
227 synchronized (TransliteratorInputMethod.class) {
\r
228 if (statusWindowOwner == this) {
\r
229 if (location == null) {
\r
230 statusWindow.setVisible(false);
\r
232 attachedLocation = new Point(location.x, location.y
\r
233 + location.height);
\r
234 pin(attachedLocation, attachedLimits);
\r
235 statusWindow.setLocation(attachedLocation);
\r
236 statusWindow.setVisible(true);
\r
240 if (TRACE_MESSAGES)
\r
241 dumpStatus("<<notify client window change: " + location);
\r
244 public void activate() {
\r
245 if (TRACE_MESSAGES)
\r
246 dumpStatus(">>activate");
\r
248 synchronized (TransliteratorInputMethod.class) {
\r
249 if (statusWindowOwner != this) {
\r
250 if (TRACE_MESSAGES)
\r
251 dumpStatus("setStatusWindowOwner from: " + statusWindowOwner + " to: " + this);
\r
253 statusWindowOwner = this;
\r
254 // will be null before first change notification
\r
255 if (attachedStatusWindow && attachedLocation != null) {
\r
256 statusWindow.setLocation(attachedLocation);
\r
258 choices.setSelectedIndex(selectedIndex == -1 ? choices
\r
259 .getSelectedIndex() : selectedIndex);
\r
262 choices.setForeground(Color.BLACK);
\r
263 statusWindow.setVisible(true);
\r
265 if (TRACE_MESSAGES)
\r
266 dumpStatus("<<activate");
\r
269 public void deactivate(boolean isTemporary) {
\r
270 if (TRACE_MESSAGES)
\r
271 dumpStatus(">>deactivate" + (isTemporary ? " (temporary)" : ""));
\r
272 if (!isTemporary) {
\r
273 synchronized (TransliteratorInputMethod.class) {
\r
274 choices.setForeground(Color.LIGHT_GRAY);
\r
277 if (TRACE_MESSAGES)
\r
278 dumpStatus("<<deactivate" + (isTemporary ? " (temporary)" : ""));
\r
281 public void hideWindows() {
\r
282 if (TRACE_MESSAGES)
\r
283 dumpStatus(">>hideWindows");
\r
284 synchronized (TransliteratorInputMethod.class) {
\r
285 if (statusWindowOwner == this) {
\r
286 if (TRACE_MESSAGES)
\r
287 dumpStatus("hiding");
\r
288 statusWindow.setVisible(false);
\r
291 if (TRACE_MESSAGES)
\r
292 dumpStatus("<<hideWindows");
\r
295 public boolean setLocale(Locale locale) {
\r
299 public Locale getLocale() {
\r
300 return Locale.getDefault();
\r
303 public void setCharacterSubsets(Character.Subset[] subsets) {
\r
306 public void reconvert() {
\r
307 throw new UnsupportedOperationException();
\r
310 public void removeNotify() {
\r
311 if (TRACE_MESSAGES)
\r
312 dumpStatus("**removeNotify");
\r
315 public void endComposition() {
\r
319 public void dispose() {
\r
320 if (TRACE_MESSAGES)
\r
321 dumpStatus("**dispose");
\r
324 public Object getControlObject() {
\r
328 public void setCompositionEnabled(boolean enable) {
\r
332 public boolean isCompositionEnabled() {
\r
337 private String eventInfo(AWTEvent event) {
\r
338 String info = event.toString();
\r
339 StringBuffer buf = new StringBuffer();
\r
340 int index1 = info.indexOf("[");
\r
341 int index2 = info.indexOf(",", index1);
\r
342 buf.append(info.substring(index1 + 1, index2));
\r
344 index1 = info.indexOf("] on ");
\r
345 index2 = info.indexOf("[", index1);
\r
346 if (index2 != -1) {
\r
347 int index3 = info.lastIndexOf(".", index2);
\r
348 if (index3 < index1 + 4) {
\r
349 index3 = index1 + 4;
\r
351 buf.append(" on ");
\r
352 buf.append(info.substring(index3 + 1, index2));
\r
354 return buf.toString();
\r
357 public void dispatchEvent(AWTEvent event) {
\r
358 final int MODIFIERS =
\r
359 InputEvent.CTRL_MASK |
\r
360 InputEvent.META_MASK |
\r
361 InputEvent.ALT_MASK |
\r
362 InputEvent.ALT_GRAPH_MASK;
\r
364 switch (event.getID()) {
\r
365 case MouseEvent.MOUSE_PRESSED:
\r
367 if (TRACE_EVENT) System.out.println("TIM: " + eventInfo(event));
\r
368 // we'll get this even if the user is scrolling, can we rely on the component?
\r
369 // commitAll(); // don't allow even clicks within our own edit area
\r
373 case KeyEvent.KEY_TYPED: {
\r
375 KeyEvent ke = (KeyEvent)event;
\r
376 if (TRACE_EVENT) System.out.println("TIM: " + eventInfo(ke));
\r
377 if ((ke.getModifiers() & MODIFIERS) != 0) {
\r
378 commitAll(); // assume a command, let it go through
\r
380 if (handleTyped(ke.getKeyChar())) {
\r
387 case KeyEvent.KEY_PRESSED: {
\r
389 KeyEvent ke = (KeyEvent)event;
\r
390 if (TRACE_EVENT) System.out.println("TIM: " + eventInfo(ke));
\r
391 if (handlePressed(ke.getKeyCode())) {
\r
397 case KeyEvent.KEY_RELEASED: {
\r
398 // this won't autorepeat, which is better for toggle actions
\r
399 KeyEvent ke = (KeyEvent)event;
\r
400 if (ke.getKeyCode() == KeyEvent.VK_SPACE && ke.isControlDown()) {
\r
401 setCompositionEnabled(!enabled);
\r
411 private void reset() {
\r
412 buffer.delete(0, buffer.length());
\r
413 index.contextStart = index.contextLimit = index.start = index.limit = 0;
\r
416 // committed}context-composed|composed
\r
420 private void traceBuffer(String msg, int cc, int off) {
\r
422 System.out.println(Utility.escape(msg + ": '"
\r
423 + buffer.substring(0, cc) + '}'
\r
424 + buffer.substring(cc, index.start) + '-'
\r
425 + buffer.substring(index.start, index.contextLimit) + '|'
\r
426 + buffer.substring(index.contextLimit) + '\''));
\r
429 private void update(boolean flush) {
\r
430 int len = buffer.length();
\r
431 String text = buffer.toString();
\r
432 AttributedString as = new AttributedString(text);
\r
436 off = index.contextLimit - len; // will be negative
\r
437 cc = index.start = index.limit = index.contextLimit = len;
\r
439 cc = index.start > desiredContext ? index.start - desiredContext
\r
441 off = index.contextLimit - cc;
\r
444 if (index.start < len) {
\r
445 as.addAttribute(TextAttribute.INPUT_METHOD_HIGHLIGHT,
\r
446 InputMethodHighlight.SELECTED_RAW_TEXT_HIGHLIGHT,
\r
450 imc.dispatchInputMethodEvent(
\r
451 InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, as.getIterator(),
\r
452 cc, TextHitInfo.leading(off), null);
\r
454 traceBuffer("update", cc, off);
\r
457 buffer.delete(0, cc);
\r
460 index.contextLimit -= cc;
\r
464 private void updateCaret() {
\r
465 imc.dispatchInputMethodEvent(InputMethodEvent.CARET_POSITION_CHANGED,
\r
466 null, 0, TextHitInfo.leading(index.contextLimit), null);
\r
467 traceBuffer("updateCaret", 0, index.contextLimit);
\r
470 private void caretToStart() {
\r
471 if (index.contextLimit > index.start) {
\r
472 index.contextLimit = index.limit = index.start;
\r
477 private void caretToLimit() {
\r
478 if (index.contextLimit < buffer.length()) {
\r
479 index.contextLimit = index.limit = buffer.length();
\r
484 private boolean caretTowardsStart() {
\r
485 int bufpos = index.contextLimit;
\r
486 if (bufpos > index.start) {
\r
488 if (bufpos > index.start
\r
489 && UCharacter.isLowSurrogate(buffer.charAt(bufpos))
\r
490 && UCharacter.isHighSurrogate(buffer.charAt(bufpos - 1))) {
\r
493 index.contextLimit = index.limit = bufpos;
\r
497 return commitAll();
\r
500 private boolean caretTowardsLimit() {
\r
501 int bufpos = index.contextLimit;
\r
502 if (bufpos < buffer.length()) {
\r
504 if (bufpos < buffer.length()
\r
505 && UCharacter.isLowSurrogate(buffer.charAt(bufpos))
\r
506 && UCharacter.isHighSurrogate(buffer.charAt(bufpos - 1))) {
\r
509 index.contextLimit = index.limit = bufpos;
\r
513 return commitAll();
\r
516 private boolean canBackspace() {
\r
517 return index.contextLimit > 0;
\r
520 private boolean backspace() {
\r
521 int bufpos = index.contextLimit;
\r
523 int limit = bufpos;
\r
525 if (bufpos > 0 && UCharacter.isLowSurrogate(buffer.charAt(bufpos))
\r
526 && UCharacter.isHighSurrogate(buffer.charAt(bufpos - 1))) {
\r
529 if (bufpos < index.start) {
\r
530 index.start = bufpos;
\r
532 index.contextLimit = index.limit = bufpos;
\r
533 doDelete(bufpos, limit);
\r
539 private boolean canDelete() {
\r
540 return index.contextLimit < buffer.length();
\r
543 private boolean delete() {
\r
544 int bufpos = index.contextLimit;
\r
545 if (bufpos < buffer.length()) {
\r
546 int limit = bufpos + 1;
\r
547 if (limit < buffer.length()
\r
548 && UCharacter.isHighSurrogate(buffer.charAt(limit - 1))
\r
549 && UCharacter.isLowSurrogate(buffer.charAt(limit))) {
\r
552 doDelete(bufpos, limit);
\r
558 private void doDelete(int start, int limit) {
\r
559 buffer.delete(start, limit);
\r
563 private boolean commitAll() {
\r
564 if (buffer.length() > 0) {
\r
565 boolean atStart = index.start == index.contextLimit;
\r
566 boolean didConvert = buffer.length() > index.start;
\r
567 index.contextLimit = index.limit = buffer.length();
\r
568 transliterator.finishTransliteration(replaceableText, index);
\r
570 index.start = index.limit = index.contextLimit = 0;
\r
578 private void clearAll() {
\r
579 int len = buffer.length();
\r
581 if (len > index.start) {
\r
582 buffer.delete(index.start, len);
\r
588 private boolean insert(char c) {
\r
589 transliterator.transliterate(replaceableText, index, c);
\r
594 private boolean editing() {
\r
595 return buffer.length() > 0;
\r
599 * The big problem is that from release to release swing changes how it
\r
600 * handles some characters like tab and backspace. Sometimes it handles
\r
601 * them as keyTyped events, and sometimes it handles them as keyPressed
\r
602 * events. If you want to allow the event to go through so swing handles
\r
603 * it, you have to allow one or the other to go through. If you don't want
\r
604 * the event to go through so you can handle it, you have to stop the
\r
605 * event both places.
\r
606 * @return whether the character was handled
\r
608 private boolean handleTyped(char ch) {
\r
611 case '\b': if (editing()) return backspace(); break;
\r
612 case '\t': if (editing()) { return commitAll(); } break;
\r
613 case '\u001b': if (editing()) { clearAll(); return true; } break;
\r
614 case '\u007f': if (editing()) return delete(); break;
\r
615 default: return insert(ch);
\r
622 * Handle keyPressed events.
\r
624 private boolean handlePressed(int code) {
\r
625 if (enabled && editing()) {
\r
627 case KeyEvent.VK_PAGE_UP:
\r
628 case KeyEvent.VK_UP:
\r
629 case KeyEvent.VK_KP_UP:
\r
630 case KeyEvent.VK_HOME:
\r
631 caretToStart(); return true;
\r
632 case KeyEvent.VK_PAGE_DOWN:
\r
633 case KeyEvent.VK_DOWN:
\r
634 case KeyEvent.VK_KP_DOWN:
\r
635 case KeyEvent.VK_END:
\r
636 caretToLimit(); return true;
\r
637 case KeyEvent.VK_LEFT:
\r
638 case KeyEvent.VK_KP_LEFT:
\r
639 return caretTowardsStart();
\r
640 case KeyEvent.VK_RIGHT:
\r
641 case KeyEvent.VK_KP_RIGHT:
\r
642 return caretTowardsLimit();
\r
643 case KeyEvent.VK_BACK_SPACE:
\r
644 return canBackspace(); // unfortunately, in 1.5 swing handles this in keyPressed instead of keyTyped
\r
645 case KeyEvent.VK_DELETE:
\r
646 return canDelete(); // this too?
\r
647 case KeyEvent.VK_TAB:
\r
648 case KeyEvent.VK_ENTER:
\r
649 return commitAll(); // so we'll never handle VK_TAB in keyTyped
\r
651 case KeyEvent.VK_SHIFT:
\r
652 case KeyEvent.VK_CONTROL:
\r
653 case KeyEvent.VK_ALT:
\r
654 return false; // ignore these unless a key typed event gets generated
\r
656 // by default, let editor handle it, and we'll assume that it will tell us
\r
657 // to endComposition if it does anything funky with, e.g., function keys.
\r
664 public String toString() {
\r
665 final String[] names = {
\r
666 "alice", "bill", "carrie", "doug", "elena", "frank", "gertie", "howie", "ingrid", "john"
\r
669 if (id < names.length) {
\r
672 return names[id] + "-" + (id/names.length);
\r
677 class NameRenderer extends JLabel implements ListCellRenderer {
\r
680 * For serialization
\r
682 private static final long serialVersionUID = -210152863798631747L;
\r
684 public Component getListCellRendererComponent(
\r
688 boolean isSelected,
\r
689 boolean cellHasFocus) {
\r
691 String s = ((JLabel)value).getText();
\r
695 setBackground(list.getSelectionBackground());
\r
696 setForeground(list.getSelectionForeground());
\r
698 setBackground(list.getBackground());
\r
699 setForeground(list.getForeground());
\r
702 setEnabled(list.isEnabled());
\r
703 setFont(list.getFont());
\r
709 class LabelComparator implements Comparator {
\r
710 public int compare(Object obj1, Object obj2) {
\r
711 Collator collator = Collator.getInstance();
\r
712 return collator.compare(((JLabel)obj1).getText(), ((JLabel)obj2).getText());
\r
715 public boolean equals(Object obj1) {
\r
716 return this.equals(obj1);
\r