/* ******************************************************************************* * Copyright (C) 2004-2010, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ package com.ibm.icu.dev.tool.ime.translit; import java.awt.AWTEvent; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.InputEvent; import java.awt.event.InputMethodEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.font.TextAttribute; import java.awt.font.TextHitInfo; import java.awt.im.InputMethodHighlight; import java.awt.im.spi.InputMethod; import java.awt.im.spi.InputMethodContext; import java.text.AttributedString; import java.text.Collator; import java.util.Comparator; import java.util.Enumeration; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.TreeSet; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.ListCellRenderer; import com.ibm.icu.impl.Utility; import com.ibm.icu.lang.UCharacter; import com.ibm.icu.text.ReplaceableString; import com.ibm.icu.text.Transliterator; public class TransliteratorInputMethod implements InputMethod { private static boolean usesAttachedIME() { // we're in the ext directory so permissions are not an issue String os = System.getProperty("os.name"); if (os != null) { return os.indexOf("Windows") == -1; } return false; } // true if Solaris style; false if PC style, assume Apple uses PC style for now private static final boolean attachedStatusWindow = usesAttachedIME(); // the shared status window private static Window statusWindow; // current or last owner private static TransliteratorInputMethod statusWindowOwner; // cache location limits for attached private static Rectangle attachedLimits; // convenience of access, to reflect the current state private static JComboBox choices; // // per-instance state // // if we're attached, the status window follows the client window private Point attachedLocation; private static int gid; private int id = gid++; InputMethodContext imc; private boolean enabled = true; private int selectedIndex = -1; // index in JComboBox corresponding to our transliterator private Transliterator transliterator; private int desiredContext; private StringBuffer buffer; private ReplaceableString replaceableText; private Transliterator.Position index; // debugging private static boolean TRACE_EVENT = false; private static boolean TRACE_MESSAGES = false; private static boolean TRACE_BUFFER = false; public TransliteratorInputMethod() { if (TRACE_MESSAGES) dumpStatus(""); buffer = new StringBuffer(); replaceableText = new ReplaceableString(buffer); index = new Transliterator.Position(); } public void dumpStatus(String msg) { System.out.println("(" + this + ") " + msg); } public void setInputMethodContext(InputMethodContext context) { initStatusWindow(context); imc = context; imc.enableClientWindowNotification(this, attachedStatusWindow); } private static void initStatusWindow(InputMethodContext context) { if (statusWindow == null) { String title; try { ResourceBundle rb = ResourceBundle .getBundle("com.ibm.icu.dev.tool.ime.translit.Transliterator"); title = rb.getString("title"); } catch (MissingResourceException m) { System.out.println("Transliterator resources missing: " + m); title = "Transliterator Input Method"; } Window sw = context.createInputMethodWindow(title, false); // get all the ICU Transliterators Enumeration en = Transliterator.getAvailableIDs(); TreeSet types = new TreeSet(new LabelComparator()); while (en.hasMoreElements()) { String id = (String) en.nextElement(); String name = Transliterator.getDisplayName(id); JLabel label = new JLabel(name); label.setName(id); types.add(label); } // add the transliterators to the combo box choices = new JComboBox(types.toArray()); choices.setEditable(false); choices.setSelectedIndex(0); choices.setRenderer(new NameRenderer()); choices.setActionCommand("transliterator"); choices.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (statusWindowOwner != null) { statusWindowOwner.statusWindowAction(e); } } }); sw.add(choices); sw.pack(); Dimension sd = Toolkit.getDefaultToolkit().getScreenSize(); Dimension wd = sw.getSize(); if (attachedStatusWindow) { attachedLimits = new Rectangle(0, 0, sd.width - wd.width, sd.height - wd.height); } else { sw.setLocation(sd.width - wd.width, sd.height - wd.height - 25); } synchronized (TransliteratorInputMethod.class) { if (statusWindow == null) { statusWindow = sw; } } } } private void statusWindowAction(ActionEvent e) { if (TRACE_MESSAGES) dumpStatus(">>status window action"); JComboBox cb = (JComboBox) e.getSource(); int si = cb.getSelectedIndex(); if (si != selectedIndex) { // otherwise, we don't need to change if (TRACE_MESSAGES) dumpStatus("status window action oldIndex: " + selectedIndex + " newIndex: " + si); selectedIndex = si; JLabel item = (JLabel) cb.getSelectedItem(); // construct the actual transliterator // commit any text that may be present first commitAll(); transliterator = Transliterator.getInstance(item.getName()); desiredContext = transliterator.getMaximumContextLength(); reset(); } if (TRACE_MESSAGES) dumpStatus("< r.x + r.width) { p.x = r.x + r.width; } if (p.y < r.y) { p.y = r.y; } else if (p.y > r.y + r.height) { p.y = r.y + r.height; } } public void notifyClientWindowChange(Rectangle location) { if (TRACE_MESSAGES) dumpStatus(">>notify client window change: " + location); synchronized (TransliteratorInputMethod.class) { if (statusWindowOwner == this) { if (location == null) { statusWindow.setVisible(false); } else { attachedLocation = new Point(location.x, location.y + location.height); pin(attachedLocation, attachedLimits); statusWindow.setLocation(attachedLocation); statusWindow.setVisible(true); } } } if (TRACE_MESSAGES) dumpStatus("<>activate"); synchronized (TransliteratorInputMethod.class) { if (statusWindowOwner != this) { if (TRACE_MESSAGES) dumpStatus("setStatusWindowOwner from: " + statusWindowOwner + " to: " + this); statusWindowOwner = this; // will be null before first change notification if (attachedStatusWindow && attachedLocation != null) { statusWindow.setLocation(attachedLocation); } choices.setSelectedIndex(selectedIndex == -1 ? choices .getSelectedIndex() : selectedIndex); } choices.setForeground(Color.BLACK); statusWindow.setVisible(true); } if (TRACE_MESSAGES) dumpStatus("<>deactivate" + (isTemporary ? " (temporary)" : "")); if (!isTemporary) { synchronized (TransliteratorInputMethod.class) { choices.setForeground(Color.LIGHT_GRAY); } } if (TRACE_MESSAGES) dumpStatus("<>hideWindows"); synchronized (TransliteratorInputMethod.class) { if (statusWindowOwner == this) { if (TRACE_MESSAGES) dumpStatus("hiding"); statusWindow.setVisible(false); } } if (TRACE_MESSAGES) dumpStatus("< desiredContext ? index.start - desiredContext : 0; off = index.contextLimit - cc; } if (index.start < len) { as.addAttribute(TextAttribute.INPUT_METHOD_HIGHLIGHT, InputMethodHighlight.SELECTED_RAW_TEXT_HIGHLIGHT, index.start, len); } imc.dispatchInputMethodEvent( InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, as.getIterator(), cc, TextHitInfo.leading(off), null); traceBuffer("update", cc, off); if (cc > 0) { buffer.delete(0, cc); index.start -= cc; index.limit -= cc; index.contextLimit -= cc; } } private void updateCaret() { imc.dispatchInputMethodEvent(InputMethodEvent.CARET_POSITION_CHANGED, null, 0, TextHitInfo.leading(index.contextLimit), null); traceBuffer("updateCaret", 0, index.contextLimit); } private void caretToStart() { if (index.contextLimit > index.start) { index.contextLimit = index.limit = index.start; updateCaret(); } } private void caretToLimit() { if (index.contextLimit < buffer.length()) { index.contextLimit = index.limit = buffer.length(); updateCaret(); } } private boolean caretTowardsStart() { int bufpos = index.contextLimit; if (bufpos > index.start) { --bufpos; if (bufpos > index.start && UCharacter.isLowSurrogate(buffer.charAt(bufpos)) && UCharacter.isHighSurrogate(buffer.charAt(bufpos - 1))) { --bufpos; } index.contextLimit = index.limit = bufpos; updateCaret(); return true; } return commitAll(); } private boolean caretTowardsLimit() { int bufpos = index.contextLimit; if (bufpos < buffer.length()) { ++bufpos; if (bufpos < buffer.length() && UCharacter.isLowSurrogate(buffer.charAt(bufpos)) && UCharacter.isHighSurrogate(buffer.charAt(bufpos - 1))) { ++bufpos; } index.contextLimit = index.limit = bufpos; updateCaret(); return true; } return commitAll(); } private boolean canBackspace() { return index.contextLimit > 0; } private boolean backspace() { int bufpos = index.contextLimit; if (bufpos > 0) { int limit = bufpos; --bufpos; if (bufpos > 0 && UCharacter.isLowSurrogate(buffer.charAt(bufpos)) && UCharacter.isHighSurrogate(buffer.charAt(bufpos - 1))) { --bufpos; } if (bufpos < index.start) { index.start = bufpos; } index.contextLimit = index.limit = bufpos; doDelete(bufpos, limit); return true; } return false; } private boolean canDelete() { return index.contextLimit < buffer.length(); } private boolean delete() { int bufpos = index.contextLimit; if (bufpos < buffer.length()) { int limit = bufpos + 1; if (limit < buffer.length() && UCharacter.isHighSurrogate(buffer.charAt(limit - 1)) && UCharacter.isLowSurrogate(buffer.charAt(limit))) { ++limit; } doDelete(bufpos, limit); return true; } return false; } private void doDelete(int start, int limit) { buffer.delete(start, limit); update(false); } private boolean commitAll() { if (buffer.length() > 0) { boolean atStart = index.start == index.contextLimit; boolean didConvert = buffer.length() > index.start; index.contextLimit = index.limit = buffer.length(); transliterator.finishTransliteration(replaceableText, index); if (atStart) { index.start = index.limit = index.contextLimit = 0; } update(true); return didConvert; } return false; } private void clearAll() { int len = buffer.length(); if (len > 0) { if (len > index.start) { buffer.delete(index.start, len); } update(true); } } private boolean insert(char c) { transliterator.transliterate(replaceableText, index, c); update(false); return true; } private boolean editing() { return buffer.length() > 0; } /** * The big problem is that from release to release swing changes how it * handles some characters like tab and backspace. Sometimes it handles * them as keyTyped events, and sometimes it handles them as keyPressed * events. If you want to allow the event to go through so swing handles * it, you have to allow one or the other to go through. If you don't want * the event to go through so you can handle it, you have to stop the * event both places. * @return whether the character was handled */ private boolean handleTyped(char ch) { if (enabled) { switch (ch) { case '\b': if (editing()) return backspace(); break; case '\t': if (editing()) { return commitAll(); } break; case '\u001b': if (editing()) { clearAll(); return true; } break; case '\u007f': if (editing()) return delete(); break; default: return insert(ch); } } return false; } /** * Handle keyPressed events. */ private boolean handlePressed(int code) { if (enabled && editing()) { switch (code) { case KeyEvent.VK_PAGE_UP: case KeyEvent.VK_UP: case KeyEvent.VK_KP_UP: case KeyEvent.VK_HOME: caretToStart(); return true; case KeyEvent.VK_PAGE_DOWN: case KeyEvent.VK_DOWN: case KeyEvent.VK_KP_DOWN: case KeyEvent.VK_END: caretToLimit(); return true; case KeyEvent.VK_LEFT: case KeyEvent.VK_KP_LEFT: return caretTowardsStart(); case KeyEvent.VK_RIGHT: case KeyEvent.VK_KP_RIGHT: return caretTowardsLimit(); case KeyEvent.VK_BACK_SPACE: return canBackspace(); // unfortunately, in 1.5 swing handles this in keyPressed instead of keyTyped case KeyEvent.VK_DELETE: return canDelete(); // this too? case KeyEvent.VK_TAB: case KeyEvent.VK_ENTER: return commitAll(); // so we'll never handle VK_TAB in keyTyped case KeyEvent.VK_SHIFT: case KeyEvent.VK_CONTROL: case KeyEvent.VK_ALT: return false; // ignore these unless a key typed event gets generated default: // by default, let editor handle it, and we'll assume that it will tell us // to endComposition if it does anything funky with, e.g., function keys. return false; } } return false; } public String toString() { final String[] names = { "alice", "bill", "carrie", "doug", "elena", "frank", "gertie", "howie", "ingrid", "john" }; if (id < names.length) { return names[id]; } else { return names[id] + "-" + (id/names.length); } } } class NameRenderer extends JLabel implements ListCellRenderer { /** * For serialization */ private static final long serialVersionUID = -210152863798631747L; public Component getListCellRendererComponent( JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { String s = ((JLabel)value).getText(); setText(s); if (isSelected) { setBackground(list.getSelectionBackground()); setForeground(list.getSelectionForeground()); } else { setBackground(list.getBackground()); setForeground(list.getForeground()); } setEnabled(list.isEnabled()); setFont(list.getFont()); setOpaque(true); return this; } } class LabelComparator implements Comparator { public int compare(Object obj1, Object obj2) { Collator collator = Collator.getInstance(); return collator.compare(((JLabel)obj1).getText(), ((JLabel)obj2).getText()); } public boolean equals(Object obj1) { return this.equals(obj1); } }