blob: 7ce5bfd4e399b7d31e133379d147bcb065922297 [file] [log] [blame]
/*
*******************************************************************************
* 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);
}
}