001: /*
002: *******************************************************************************
003: * Copyright (C) 2004, International Business Machines Corporation and *
004: * others. All Rights Reserved. *
005: *******************************************************************************
006: */
007:
008: package com.ibm.icu.dev.tool.ime.translit;
009:
010: import java.awt.AWTEvent;
011: import java.awt.Color;
012: import java.awt.Component;
013: import java.awt.Dimension;
014: import java.awt.Point;
015: import java.awt.Rectangle;
016: import java.awt.Toolkit;
017: import java.awt.Window;
018: import java.awt.event.ActionEvent;
019: import java.awt.event.ActionListener;
020: import java.awt.event.InputEvent;
021: import java.awt.event.InputMethodEvent;
022: import java.awt.event.KeyEvent;
023: import java.awt.event.MouseEvent;
024: import java.awt.font.TextAttribute;
025: import java.awt.font.TextHitInfo;
026: import java.awt.im.InputMethodHighlight;
027: import java.awt.im.spi.InputMethod;
028: import java.awt.im.spi.InputMethodContext;
029: import java.text.AttributedString;
030: import java.util.Comparator;
031: import java.util.Enumeration;
032: import java.util.Locale;
033: import java.util.MissingResourceException;
034: import java.util.ResourceBundle;
035: import java.util.TreeSet;
036:
037: import javax.swing.JComboBox;
038: import javax.swing.JLabel;
039: import javax.swing.JList;
040: import javax.swing.ListCellRenderer;
041:
042: import com.ibm.icu.impl.Utility;
043: import com.ibm.icu.lang.UCharacter;
044: import com.ibm.icu.text.Collator;
045: import com.ibm.icu.text.ReplaceableString;
046: import com.ibm.icu.text.Transliterator;
047:
048: public class TransliteratorInputMethod implements InputMethod {
049:
050: private static boolean usesAttachedIME() {
051: // we're in the ext directory so permissions are not an issue
052: String os = System.getProperty("os.name");
053: if (os != null) {
054: return os.indexOf("Windows") == -1;
055: }
056: return false;
057: }
058:
059: // true if Solaris style; false if PC style, assume Apple uses PC style for now
060: private static final boolean attachedStatusWindow = usesAttachedIME();
061:
062: // the shared status window
063: private static Window statusWindow;
064:
065: // current or last owner
066: private static TransliteratorInputMethod statusWindowOwner;
067:
068: // cache location limits for attached
069: private static Rectangle attachedLimits;
070:
071: // convenience of access, to reflect the current state
072: private static JComboBox choices;
073:
074: //
075: // per-instance state
076: //
077:
078: // if we're attached, the status window follows the client window
079: private Point attachedLocation;
080:
081: private static int gid;
082:
083: private int id = gid++;
084:
085: InputMethodContext imc;
086: private boolean enabled = true;
087:
088: private int selectedIndex = -1; // index in JComboBox corresponding to our transliterator
089: private Transliterator transliterator;
090: private int desiredContext;
091: private StringBuffer buffer;
092: private ReplaceableString replaceableText;
093: private Transliterator.Position index;
094:
095: // debugging
096: private static boolean TRACE_EVENT = false;
097: private static boolean TRACE_MESSAGES = false;
098: private static boolean TRACE_BUFFER = false;
099:
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: "
129: + m);
130: title = "Transliterator Input Method";
131: }
132:
133: Window sw = context.createInputMethodWindow(title, false);
134:
135: // get all the ICU Transliterators
136: Enumeration e = Transliterator.getAvailableIDs();
137: TreeSet types = new TreeSet(new LabelComparator());
138:
139: while (e.hasMoreElements()) {
140: String id = (String) e.nextElement();
141: String name = Transliterator.getDisplayName(id);
142: JLabel label = new JLabel(name);
143: label.setName(id);
144: types.add(label);
145: }
146:
147: // add the transliterators to the combo box
148:
149: choices = new JComboBox(types.toArray());
150:
151: choices.setEditable(false);
152: choices.setSelectedIndex(0);
153: choices.setRenderer(new NameRenderer());
154: choices.setActionCommand("transliterator");
155:
156: choices.addActionListener(new ActionListener() {
157: public void actionPerformed(ActionEvent e) {
158: if (statusWindowOwner != null) {
159: statusWindowOwner.statusWindowAction(e);
160: }
161: }
162: });
163:
164: sw.add(choices);
165: sw.pack();
166:
167: Dimension sd = Toolkit.getDefaultToolkit().getScreenSize();
168: Dimension wd = sw.getSize();
169: if (attachedStatusWindow) {
170: attachedLimits = new Rectangle(0, 0, sd.width
171: - wd.width, sd.height - wd.height);
172: } else {
173: sw.setLocation(sd.width - wd.width, sd.height
174: - wd.height - 25);
175: }
176:
177: synchronized (TransliteratorInputMethod.class) {
178: if (statusWindow == null) {
179: statusWindow = sw;
180: }
181: }
182: }
183: }
184:
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: "
193: + selectedIndex + " 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?
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:
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:
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: "
254: + statusWindowOwner + " to: " + this );
255:
256: statusWindowOwner = this ;
257: if (attachedStatusWindow && attachedLocation != null) { // will be null before first change notification
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:
271: public void deactivate(boolean isTemporary) {
272: if (TRACE_MESSAGES)
273: dumpStatus(">>deactivate"
274: + (isTemporary ? " (temporary)" : ""));
275: if (!isTemporary) {
276: synchronized (TransliteratorInputMethod.class) {
277: choices.setForeground(Color.LIGHT_GRAY);
278: }
279: }
280: if (TRACE_MESSAGES)
281: dumpStatus("<<deactivate"
282: + (isTemporary ? " (temporary)" : ""));
283: }
284:
285: public void hideWindows() {
286: if (TRACE_MESSAGES)
287: dumpStatus(">>hideWindows");
288: synchronized (TransliteratorInputMethod.class) {
289: if (statusWindowOwner == this ) {
290: if (TRACE_MESSAGES)
291: dumpStatus("hiding");
292: statusWindow.setVisible(false);
293: }
294: }
295: if (TRACE_MESSAGES)
296: dumpStatus("<<hideWindows");
297: }
298:
299: public boolean setLocale(Locale locale) {
300: return false;
301: }
302:
303: public Locale getLocale() {
304: return Locale.getDefault();
305: }
306:
307: public void setCharacterSubsets(Character.Subset[] subsets) {
308: }
309:
310: public void reconvert() {
311: throw new UnsupportedOperationException();
312: }
313:
314: public void removeNotify() {
315: if (TRACE_MESSAGES)
316: dumpStatus("**removeNotify");
317: }
318:
319: public void endComposition() {
320: commitAll();
321: }
322:
323: public void dispose() {
324: if (TRACE_MESSAGES)
325: dumpStatus("**dispose");
326: }
327:
328: public Object getControlObject() {
329: return null;
330: }
331:
332: public void setCompositionEnabled(boolean enable) {
333: enabled = enable;
334: }
335:
336: public boolean isCompositionEnabled() {
337: return enabled;
338: }
339:
340: // debugging
341: private String eventInfo(AWTEvent event) {
342: String info = event.toString();
343: StringBuffer buf = new StringBuffer();
344: int index1 = info.indexOf("[");
345: int index2 = info.indexOf(",", index1);
346: buf.append(info.substring(index1 + 1, index2));
347:
348: index1 = info.indexOf("] on ");
349: index2 = info.indexOf("[", index1);
350: if (index2 != -1) {
351: int index3 = info.lastIndexOf(".", index2);
352: if (index3 < index1 + 4) {
353: index3 = index1 + 4;
354: }
355: buf.append(" on ");
356: buf.append(info.substring(index3 + 1, index2));
357: }
358: return buf.toString();
359: }
360:
361: public void dispatchEvent(AWTEvent event) {
362: final int MODIFIERS = InputEvent.CTRL_MASK
363: | InputEvent.META_MASK | 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)
370: System.out.println("TIM: " + eventInfo(event));
371: // we'll get this even if the user is scrolling, can we rely on the component?
372: // commitAll(); // don't allow even clicks within our own edit area
373: }
374: break;
375:
376: case KeyEvent.KEY_TYPED: {
377: if (enabled) {
378: KeyEvent ke = (KeyEvent) event;
379: if (TRACE_EVENT)
380: System.out.println("TIM: " + eventInfo(ke));
381: if ((ke.getModifiers() & MODIFIERS) != 0) {
382: commitAll(); // assume a command, let it go through
383: } else {
384: if (handleTyped(ke.getKeyChar())) {
385: ke.consume();
386: }
387: }
388: }
389: }
390: break;
391:
392: case KeyEvent.KEY_PRESSED: {
393: if (enabled) {
394: KeyEvent ke = (KeyEvent) event;
395: if (TRACE_EVENT)
396: System.out.println("TIM: " + eventInfo(ke));
397: if (handlePressed(ke.getKeyCode())) {
398: ke.consume();
399: }
400: }
401: }
402: break;
403:
404: case KeyEvent.KEY_RELEASED: {
405: // this won't autorepeat, which is better for toggle actions
406: KeyEvent ke = (KeyEvent) event;
407: if (ke.getKeyCode() == KeyEvent.VK_SPACE
408: && ke.isControlDown()) {
409: setCompositionEnabled(!enabled);
410: }
411: }
412: break;
413:
414: default:
415: break;
416: }
417: }
418:
419: /** Wipe clean */
420: private void reset() {
421: buffer.delete(0, buffer.length());
422: index.contextStart = index.contextLimit = index.start = index.limit = 0;
423: }
424:
425: // committed}context-composed|composed
426: // ^ ^ ^
427: // cc start ctxLim
428:
429: private void traceBuffer(String msg, int cc, int off) {
430: if (TRACE_BUFFER)
431: System.out.println(Utility
432: .escape(msg
433: + ": '"
434: + buffer.substring(0, cc)
435: + '}'
436: + buffer.substring(cc, index.start)
437: + '-'
438: + buffer.substring(index.start,
439: index.contextLimit) + '|'
440: + buffer.substring(index.contextLimit)
441: + '\''));
442: }
443:
444: private void update(boolean flush) {
445: int len = buffer.length();
446: String text = buffer.toString();
447: AttributedString as = new AttributedString(text);
448:
449: int cc, off;
450: if (flush) {
451: off = index.contextLimit - len; // will be negative
452: cc = index.start = index.limit = index.contextLimit = len;
453: } else {
454: cc = index.start > desiredContext ? index.start
455: - desiredContext : 0;
456: off = index.contextLimit - cc;
457: }
458:
459: if (index.start < len) {
460: as.addAttribute(TextAttribute.INPUT_METHOD_HIGHLIGHT,
461: InputMethodHighlight.SELECTED_RAW_TEXT_HIGHLIGHT,
462: index.start, len);
463: }
464:
465: imc.dispatchInputMethodEvent(
466: InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, as
467: .getIterator(), cc, TextHitInfo.leading(off),
468: null);
469:
470: traceBuffer("update", cc, off);
471:
472: if (cc > 0) {
473: buffer.delete(0, cc);
474: index.start -= cc;
475: index.limit -= cc;
476: index.contextLimit -= cc;
477: }
478: }
479:
480: private void updateCaret() {
481: imc.dispatchInputMethodEvent(
482: InputMethodEvent.CARET_POSITION_CHANGED, null, 0,
483: TextHitInfo.leading(index.contextLimit), null);
484: traceBuffer("updateCaret", 0, index.contextLimit);
485: }
486:
487: private void caretToStart() {
488: if (index.contextLimit > index.start) {
489: index.contextLimit = index.limit = index.start;
490: updateCaret();
491: }
492: }
493:
494: private void caretToLimit() {
495: if (index.contextLimit < buffer.length()) {
496: index.contextLimit = index.limit = buffer.length();
497: updateCaret();
498: }
499: }
500:
501: private boolean caretTowardsStart() {
502: int bufpos = index.contextLimit;
503: if (bufpos > index.start) {
504: --bufpos;
505: if (bufpos > index.start
506: && UCharacter.isLowSurrogate(buffer.charAt(bufpos))
507: && UCharacter.isHighSurrogate(buffer
508: .charAt(bufpos - 1))) {
509: --bufpos;
510: }
511: index.contextLimit = index.limit = bufpos;
512: updateCaret();
513: return true;
514: }
515: return commitAll();
516: }
517:
518: private boolean caretTowardsLimit() {
519: int bufpos = index.contextLimit;
520: if (bufpos < buffer.length()) {
521: ++bufpos;
522: if (bufpos < buffer.length()
523: && UCharacter.isLowSurrogate(buffer.charAt(bufpos))
524: && UCharacter.isHighSurrogate(buffer
525: .charAt(bufpos - 1))) {
526: ++bufpos;
527: }
528: index.contextLimit = index.limit = bufpos;
529: updateCaret();
530: return true;
531: }
532: return commitAll();
533: }
534:
535: private boolean canBackspace() {
536: return index.contextLimit > 0;
537: }
538:
539: private boolean backspace() {
540: int bufpos = index.contextLimit;
541: if (bufpos > 0) {
542: int limit = bufpos;
543: --bufpos;
544: if (bufpos > 0
545: && UCharacter.isLowSurrogate(buffer.charAt(bufpos))
546: && UCharacter.isHighSurrogate(buffer
547: .charAt(bufpos - 1))) {
548: --bufpos;
549: }
550: if (bufpos < index.start) {
551: index.start = bufpos;
552: }
553: index.contextLimit = index.limit = bufpos;
554: doDelete(bufpos, limit);
555: return true;
556: }
557: return false;
558: }
559:
560: private boolean canDelete() {
561: return index.contextLimit < buffer.length();
562: }
563:
564: private boolean delete() {
565: int bufpos = index.contextLimit;
566: if (bufpos < buffer.length()) {
567: int limit = bufpos + 1;
568: if (limit < buffer.length()
569: && UCharacter.isHighSurrogate(buffer
570: .charAt(limit - 1))
571: && UCharacter.isLowSurrogate(buffer.charAt(limit))) {
572: ++limit;
573: }
574: doDelete(bufpos, limit);
575: return true;
576: }
577: return false;
578: }
579:
580: private void doDelete(int start, int limit) {
581: buffer.delete(start, limit);
582: update(false);
583: }
584:
585: private boolean commitAll() {
586: if (buffer.length() > 0) {
587: boolean atStart = index.start == index.contextLimit;
588: boolean didConvert = buffer.length() > index.start;
589: index.contextLimit = index.limit = buffer.length();
590: transliterator
591: .finishTransliteration(replaceableText, index);
592: if (atStart) {
593: index.start = index.limit = index.contextLimit = 0;
594: }
595: update(true);
596: return didConvert;
597: }
598: return false;
599: }
600:
601: private void clearAll() {
602: int len = buffer.length();
603: if (len > 0) {
604: if (len > index.start) {
605: buffer.delete(index.start, len);
606: }
607: update(true);
608: }
609: }
610:
611: private boolean insert(char c) {
612: transliterator.transliterate(replaceableText, index, c);
613: update(false);
614: return true;
615: }
616:
617: private boolean editing() {
618: return buffer.length() > 0;
619: }
620:
621: /**
622: * The big problem is that from release to release swing changes how it
623: * handles some characters like tab and backspace. Sometimes it handles
624: * them as keyTyped events, and sometimes it handles them as keyPressed
625: * events. If you want to allow the event to go through so swing handles
626: * it, you have to allow one or the other to go through. If you don't want
627: * the event to go through so you can handle it, you have to stop the
628: * event both places.
629: * @return whether the character was handled
630: */
631: private boolean handleTyped(char ch) {
632: if (enabled) {
633: switch (ch) {
634: case '\b':
635: if (editing())
636: return backspace();
637: break;
638: case '\t':
639: if (editing()) {
640: return commitAll();
641: }
642: break;
643: case '\u001b':
644: if (editing()) {
645: clearAll();
646: return true;
647: }
648: break;
649: case '\u007f':
650: if (editing())
651: return delete();
652: break;
653: default:
654: return insert(ch);
655: }
656: }
657: return false;
658: }
659:
660: /**
661: * Handle keyPressed events.
662: */
663: private boolean handlePressed(int code) {
664: if (enabled && editing()) {
665: switch (code) {
666: case KeyEvent.VK_PAGE_UP:
667: case KeyEvent.VK_UP:
668: case KeyEvent.VK_KP_UP:
669: case KeyEvent.VK_HOME:
670: caretToStart();
671: return true;
672: case KeyEvent.VK_PAGE_DOWN:
673: case KeyEvent.VK_DOWN:
674: case KeyEvent.VK_KP_DOWN:
675: case KeyEvent.VK_END:
676: caretToLimit();
677: return true;
678: case KeyEvent.VK_LEFT:
679: case KeyEvent.VK_KP_LEFT:
680: return caretTowardsStart();
681: case KeyEvent.VK_RIGHT:
682: case KeyEvent.VK_KP_RIGHT:
683: return caretTowardsLimit();
684: case KeyEvent.VK_BACK_SPACE:
685: return canBackspace(); // unfortunately, in 1.5 swing handles this in keyPressed instead of keyTyped
686: case KeyEvent.VK_DELETE:
687: return canDelete(); // this too?
688: case KeyEvent.VK_TAB:
689: case KeyEvent.VK_ENTER:
690: return commitAll(); // so we'll never handle VK_TAB in keyTyped
691:
692: case KeyEvent.VK_SHIFT:
693: case KeyEvent.VK_CONTROL:
694: case KeyEvent.VK_ALT:
695: return false; // ignore these unless a key typed event gets generated
696: default:
697: // by default, let editor handle it, and we'll assume that it will tell us
698: // to endComposition if it does anything funky with, e.g., function keys.
699: return false;
700: }
701: }
702: return false;
703: }
704:
705: public String toString() {
706: final String[] names = { "alice", "bill", "carrie", "doug",
707: "elena", "frank", "gertie", "howie", "ingrid", "john" };
708:
709: if (id < names.length) {
710: return names[id];
711: } else {
712: return names[id] + "-" + (id / names.length);
713: }
714: }
715: }
716:
717: class NameRenderer extends JLabel implements ListCellRenderer {
718:
719: public Component getListCellRendererComponent(JList list,
720: Object value, int index, boolean isSelected,
721: boolean cellHasFocus) {
722:
723: String s = ((JLabel) value).getText();
724: setText(s);
725:
726: if (isSelected) {
727: setBackground(list.getSelectionBackground());
728: setForeground(list.getSelectionForeground());
729: } else {
730: setBackground(list.getBackground());
731: setForeground(list.getForeground());
732: }
733:
734: setEnabled(list.isEnabled());
735: setFont(list.getFont());
736: setOpaque(true);
737: return this ;
738: }
739: }
740:
741: class LabelComparator implements Comparator {
742: public int compare(Object obj1, Object obj2) {
743: Collator collator = Collator.getInstance();
744: return collator.compare(((JLabel) obj1).getText(),
745: ((JLabel) obj2).getText());
746: }
747:
748: public boolean equals(Object obj1) {
749: return this.equals(obj1);
750: }
751: }
|