| /* |
| ******************************************************************************* |
| * Copyright (C) 2004, 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.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.Collator; |
| 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("<constructor>"); |
| |
| 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 e = Transliterator.getAvailableIDs(); |
| TreeSet types = new TreeSet(new LabelComparator()); |
| |
| while(e.hasMoreElements()) { |
| String id = (String) e.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("<<status window action"); |
| } |
| |
| // java has no pin to rectangle function? |
| private static void pin(Point p, Rectangle r) { |
| if (p.x < r.x) { p.x = r.x; } else if (p.x > 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("<<notify client window change: " + location); |
| } |
| |
| public void activate() { |
| if (TRACE_MESSAGES) dumpStatus(">>activate"); |
| |
| synchronized (TransliteratorInputMethod.class) { |
| if (statusWindowOwner != this) { |
| if (TRACE_MESSAGES) dumpStatus("setStatusWindowOwner from: " + statusWindowOwner + " to: " + this); |
| |
| statusWindowOwner = this; |
| if (attachedStatusWindow && attachedLocation != null) { // will be null before first change notification |
| statusWindow.setLocation(attachedLocation); |
| } |
| choices.setSelectedIndex(selectedIndex == -1 ? choices.getSelectedIndex() : selectedIndex); |
| } |
| |
| choices.setForeground(Color.BLACK); |
| statusWindow.setVisible(true); |
| } |
| if (TRACE_MESSAGES) dumpStatus("<<activate"); |
| } |
| |
| public void deactivate(boolean isTemporary) { |
| if (TRACE_MESSAGES) dumpStatus(">>deactivate" + (isTemporary ? " (temporary)" : "")); |
| if (!isTemporary) { |
| synchronized(TransliteratorInputMethod.class) { |
| choices.setForeground(Color.LIGHT_GRAY); |
| } |
| } |
| if (TRACE_MESSAGES) dumpStatus("<<deactivate" + (isTemporary ? " (temporary)" : "")); |
| } |
| |
| public void hideWindows() { |
| if (TRACE_MESSAGES) dumpStatus(">>hideWindows"); |
| synchronized (TransliteratorInputMethod.class) { |
| if (statusWindowOwner == this) { |
| if (TRACE_MESSAGES) dumpStatus("hiding"); |
| statusWindow.setVisible(false); |
| } |
| } |
| if (TRACE_MESSAGES) dumpStatus("<<hideWindows"); |
| } |
| |
| public boolean setLocale(Locale locale) { |
| return false; |
| } |
| |
| public Locale getLocale() { |
| return Locale.getDefault(); |
| } |
| |
| public void setCharacterSubsets(Character.Subset[] subsets) { |
| } |
| |
| public void reconvert() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public void removeNotify() { |
| if (TRACE_MESSAGES) dumpStatus("**removeNotify"); |
| } |
| |
| public void endComposition() { |
| commitAll(); |
| } |
| |
| public void dispose() { |
| if (TRACE_MESSAGES) dumpStatus("**dispose"); |
| } |
| |
| public Object getControlObject() { |
| return null; |
| } |
| |
| public void setCompositionEnabled(boolean enable) { |
| enabled = enable; |
| } |
| |
| public boolean isCompositionEnabled() { |
| return enabled; |
| } |
| |
| // debugging |
| private String eventInfo(AWTEvent event) { |
| String info = event.toString(); |
| StringBuffer buf = new StringBuffer(); |
| int index1 = info.indexOf("["); |
| int index2 = info.indexOf(",", index1); |
| buf.append(info.substring(index1+1, index2)); |
| |
| index1 = info.indexOf("] on "); |
| index2 = info.indexOf("[", index1); |
| if (index2 != -1) { |
| int index3 = info.lastIndexOf(".", index2); |
| if (index3 < index1 + 4) { |
| index3 = index1 + 4; |
| } |
| buf.append(" on "); |
| buf.append(info.substring(index3+1, index2)); |
| } |
| return buf.toString(); |
| } |
| |
| public void dispatchEvent(AWTEvent event) { |
| final int MODIFIERS = |
| InputEvent.CTRL_MASK | |
| InputEvent.META_MASK | |
| InputEvent.ALT_MASK | |
| InputEvent.ALT_GRAPH_MASK; |
| |
| switch (event.getID()) { |
| case MouseEvent.MOUSE_PRESSED: |
| if (enabled) { |
| if (TRACE_EVENT) System.out.println("TIM: " + eventInfo(event)); |
| // we'll get this even if the user is scrolling, can we rely on the component? |
| // commitAll(); // don't allow even clicks within our own edit area |
| } |
| break; |
| |
| case KeyEvent.KEY_TYPED: { |
| if (enabled) { |
| KeyEvent ke = (KeyEvent)event; |
| if (TRACE_EVENT) System.out.println("TIM: " + eventInfo(ke)); |
| if ((ke.getModifiers() & MODIFIERS) != 0) { |
| commitAll(); // assume a command, let it go through |
| } else { |
| if (handleTyped(ke.getKeyChar())) { |
| ke.consume(); |
| } |
| } |
| } |
| } break; |
| |
| case KeyEvent.KEY_PRESSED: { |
| if (enabled) { |
| KeyEvent ke = (KeyEvent)event; |
| if (TRACE_EVENT) System.out.println("TIM: " + eventInfo(ke)); |
| if (handlePressed(ke.getKeyCode())) { |
| ke.consume(); |
| } |
| } |
| } break; |
| |
| case KeyEvent.KEY_RELEASED: { |
| // this won't autorepeat, which is better for toggle actions |
| KeyEvent ke = (KeyEvent)event; |
| if (ke.getKeyCode() == KeyEvent.VK_SPACE && ke.isControlDown()) { |
| setCompositionEnabled(!enabled); |
| } |
| } break; |
| |
| default: |
| break; |
| } |
| } |
| |
| /** Wipe clean */ |
| private void reset() { |
| buffer.delete(0, buffer.length()); |
| index.contextStart = index.contextLimit = index.start = index.limit = 0; |
| } |
| |
| // committed}context-composed|composed |
| // ^ ^ ^ |
| // cc start ctxLim |
| |
| private void traceBuffer(String msg, int cc, int off) { |
| if (TRACE_BUFFER) System.out.println(Utility.escape(msg + ": '" + buffer.substring(0, cc) + '}' + |
| buffer.substring(cc, index.start) + '-' + |
| buffer.substring(index.start, index.contextLimit) + '|' + |
| buffer.substring(index.contextLimit) + '\'')); |
| } |
| |
| private void update(boolean flush) { |
| int len = buffer.length(); |
| String text = buffer.toString(); |
| AttributedString as = new AttributedString(text); |
| |
| int cc, off; |
| if (flush) { |
| off = index.contextLimit - len; // will be negative |
| cc = index.start = index.limit = index.contextLimit = len; |
| } else { |
| cc = index.start > 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 { |
| |
| 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); |
| } |
| } |
| |
| |