JAVA InputMethod 输入实现纪要
Jre1.7对输入法的支持使得java开发者能够方便地使用JAVA编写输入法,并整合适配了本地输入法,然后提供出一个在所有输入法中切换的菜单界面,并在以后的编辑文本过程中实现了一个高效的事件处理框架,最终实现了方便地利用输入法进行输入的用户体验。
一.编写Java输入法及切换输入法支持。
利用JAVA编写输入法只需实现两个核心接口:InputMethod,InputMethodDescriptor,然后将jar包放在jre/lib/ext下即可被检测到;本地系统输入法虽然可能安装多个(比如流行的谷歌输入法,紫光拼音输入法),但通过InputMethodAdapter只适配为一个输入法;各个java编写的输入法与这个本地输入法一起,将在显示输入法切换菜单InputMethodPopupMenu时作为其中的菜单项。
检测建立各个输入法的过程在任何一个窗口被初始化时完成,具体在:WFramePeer/WDialogPeer在被Toolkit.create时即会要求延迟创建InputMethodManager实例-InputMethodManager.getInstance(),整个JVM中将仅存在它一个全局单例,用来管理所有输入法,真正创建时将首先通过inputmethodadapter建立本地输入法,再检测ext中是否存在JAVA编写的输入法jar包并逐次进行加载建立,还会开启一个dameon线程:AWT-InputMethodManager。这个线程实现了显示切换输入法菜单的控制。具体为它将不断检查是否有请求显示切换输入法的菜单-InputMethodPopupMenu,发现该请求即会在该窗口上显示该菜单;请求来自下述两个方面:
第一是Toolkit.createPeer时还要InputMethodManager.getTriggerMenuString
(属性”AWT.InputMethodSelectionMenu”)来获取切换输入法的菜单名称,这个是作为peer窗口基本菜单的一项(Windows系统下点击JFrame左上角图标即显示该菜单),点击此菜单项将请求AWT-InputMethodManager显示输入法切换菜单InputMethodPopupMenu。
第二是系统会监听某定义好的切换输入法的热键(在用户preference中存在/java/awt/im/selectionKey路径下定义),热键产生时也将请求AWT-InputMethodManager显示输入法切换菜单。
最后要注意的是如果没有java输入法的jar,则AWT-InputMethodManager线程是不会建立的,同样也不会有那个窗口基本菜单项,包括热键响应,总之,没有java输入法,就不会存在切换的实现支持。
二.输入法参与的事件响应框架
a.每一个组件Component都将持有一个InputContext对象,该对象将记录当前正在使用的输入法,并在发生输入法切换或组件焦点切换的时候得到通知以调整输入法,最终协调输入法帮助完成组件的文本输入。InputContext被组件通过getInputContext()方法延迟创建。
注意对每一个Window组件,调此getInputContext方法将实实在在地延迟创建一个sun.awt.im.InputMethodContext并私自持有,而一般组件将调用parent.getInputContext。所以,同一个window下的所有组件共享一个InputContext。
b.当前输入法总是服务于当前组件Component,按照Java输入法规范,将Component分为5种情况,组件所属种类不同,输入法参与的事件响应也有所不同。
1. Active-client on-the-spot
2. Active-client below-the-spot
3. Passive-Clients
4. Non-Clients
5. Peer-text-component
Active-client是实现了getInputMethodRequests方法返回一个非空对象的Component,并且该component提供InputMethodListener即,这些组件将和inputmethod进行交互响应;on-the-spot还是below-the-spot是由系统属性确定,具体由InputContext在类加载初始化时确定:
static {
// check whether we should use below-the-spot input
// get property from command line
String inputStyle = (String) AccessController.doPrivileged
(new GetPropertyAction("java.awt.im.style", null));
// get property from awt.properties file
if (inputStyle == null) {
inputStyle = Toolkit.getDefaultToolkit().
getProperty("java.awt.im.style", null);
}
belowTheSpotInputRequested = "below-the-spot".equals(inputStyle);
}
on-the-spot是直接利用组件界面作为输入组装的style;而below-the-spot则是利用一个新的文本组装窗体compositionarea作为输入文本组装的style,并且该窗体还将随组件文本录入而自动位置尾随。
Non-Clients是那些enableInputMethods=false的组件,是要求不能使用输入法进行输入的组件;
Peer-text-component则是那些对等组件,他们的输入法相关行为依托于具体的底层系统,这里不再考虑。
Passive-Clients就是那些enableInputMethods=true,但是getInputMethodRequests又返回一个空的组件,它可以接受输入法的处理结果,但只相信这只是一般的键盘输入而不认为存在什么输入法。
c.输入法参与的事件响应场景具体如下:
首先区分当前输入法的两种情况:java输入法和本地输入法。对java输入法,其本身可能需要使用额外的窗口组件来做输入辅助,像多音汉字需要提供选择列,但是一定采用window来作为容器,并且该window是focusenable=false,所以焦点总是不会转移到输入法的这些组件上。这样当一个text组件聚焦后即使鼠标点击输入法给出的框框板板,焦点仍然不会迁移。既然目标组件保留着焦点,则后续的键盘事件也将target到该text组件上来进行处理;对本地输入法,如果是windows系统,输入法事件也会target到该组件上来。比如WInputMethod在激活后将在awt-windows线程里对底层事件处理,其处理后将post给EDT,并target到当前clientcomponent。
所以事件响应都入口到当前组件的component.dispatchEventImpl的这个方法中来:
if (areInputMethodsEnabled()) {//non-client不会得到输入法处理,直接后续进入组件的listener处理
// We need to pass on InputMethodEvents since some host
// input method adapters send them through the Java
// event queue instead of directly to the component,
// and the input context also handles the Java composition window
if(((e instanceof InputMethodEvent) && !(thisinstanceof CompositionArea)) //如果本地输入法激活则输入法已经处理基本输入事件并post合成的InputMethodEvent到这里
||
// Otherwise, we only pass on input and focus events, because
// a) input methods shouldn't know about semantic or component-level events
// b) passing on the events takes time
// c) isConsumed() is always true for semantic events.
(e instanceof InputEvent) || (e instanceof FocusEvent)) {//如果Java输入法激活将在此处接收基本输入事件
InputContext inputContext = getInputContext();//输入事件将交由InputContext进行处理。
if (inputContext != null) {
inputContext.dispatchEvent(e);
if (e.isConsumed()) {
if ((e instanceof FocusEvent) && focusLog.isLoggable(Level.FINEST)) {
focusLog.log(Level.FINEST, "3579: Skipping " + e);
}
return;
}
}
}
}
/*
* 6. Deliver event for normal processing
*/
if (newEventsOnly) {
// Filtering needs to really be moved to happen at a lower
// level in order to get maximum performance gain; it is
// here temporarily to ensure the API spec is honored.
//
if (eventEnabled(e)) {
processEvent(e); //inputmethodevent或者其他inputevent在这里交给组件的Listener处理
}
}
在整个事件处理过程中,按java inputmethod规范产生了2次事件流转分支。下面举个具体分析,其中系统输入法假定使用Windows本地输入法winputmethod,java输入法假定使用CodePointInputMethod(对unicode编码解析成对应字符的一种输入法)。Winputmethod在激活后将在awt-windows直接处理输入底层输入事件,并包装成InputMethodEvent递交给EDT进入Component做处理(注意虽然将底层事件处理合成了InputMethodEvent,但仍然会有键盘事件或鼠标事件递交给EDT)。Component会将这种InputMethodEvent,以及各个InputEvent, FocusEvent都发送给sun.awt.im. InputMethodContext(Component.getInputContext).dispatchEvent,在那里对于那些InputMethodEvent做一层过滤处理,即判断clientcomponent是否Active-below-the-spot或Passive就产生了分支,若是则直接getCompositionAreaHandler(true).processInputMethodEvent,若否则和一般的InputEvent, FocusEvent一样都将提交给sun.awt.im. InputContext.dispatchEvent,在那里将把这个InputMethodEvent(of Active-on-the-spot-Client)直接推给componet的inputmethodlistener进行处理完事,对FocusGained,FocusLost,以及可能的切换输入法的HotKey会集中精力处理,对那些InputEvent则将调用当前输入法进行处理getCurrentInputMethod.dispatchEvent.如当前输入法是windows本地输入法winputmethod,这时再次交给本地代码做一些处理handleNativeIMEEvent (awtFocussedComponentPeer, e)后随即consume;若是java输入法CodePointInputMethod,将根据自身的输入法规则进行真正的输入法拼写转换处理,之后该基本输入事件consume掉,并根据情况调用sun.awt.im.InputMethodContext.dispatchInputMethodEvent,在那里将合成InputMethodEvent(此时这个合成的InputMethodEvent相当于本地输入法一早生成的那个InputMethodEvent),再判断clientcomponent是否Active-on-the-spot-Client后产生分支,若是则直接推送给组件的对应inputmethodlistener处理;若否即Active-below-the-spot或Passive的则getCompositionAreaHandler(true).processInputMethodEvent。
那些交给CompositionAreaHandler处理的所有InputMethodEvent事件会被分类处理,对caretchange(插入符位置改变)的情况将直接处理并consume掉旧事件但不会生成新事件,对commitedTextChange(输入文本改变)的情况将consume掉旧事件,并继续根据client性质不同第二次分支合成新事件,即对Active-below-the-spot-Client合成InputMethodEvent,对Passive-Client合成KeyEvent,这两种事件将后续进入Component的对应InputMethodListener/KeyEventListener得到对应处理。
整个流程的图形流转可以参见java的inputmethod实现规范。
注意:由上述分析可见,jre1.7按照Java输入法规范实现的这种事件流,对于本地输入法与java输入法还是有区分的。区分在本地输入法在awt-windows中底层处理基本输入时即产生InputMethodEvent;而java输入法则要在EDT中得到基本的输入事件,具体为InputEvent经过了InputContext的处理后再进入java输入法,java输入法再借助InputMethodContext合成了InputMethodEvent。
注意:第一个分支存在的原因是因为Active-On-the-spot不需要使用额外的CompositionArea,所以不需要将事件流转到对应的CompositionAreaHandler,而直接进入Component的Listener。第二个分支存在的原因是因为passive既然不提供request对象,那么也不能期望其实现了Inputmethodlistener,所以只能给它的keyeventlistener进行处理。
补充:
对于java输入法-CodePointInputMethod,在得到基本输入事件dispatchEvent时是大显身手的时候。现在对CodePointInputMethod分析这一段处理过程以获悉开发java输入法的一般思路:
CodePointInputMethod. dispatchEvent将
if (!(event instanceof KeyEvent)) {//忽略KeyEvent之外的事件
return;
}
KeyEvent e = (KeyEvent) event;
int eventID = event.getID();
boolean notInCompositionMode = buffer.length() == 0;
if (eventID == KeyEvent.KEY_PRESSED) {//如果当前没有在拼写状态,忽略该事件
if (notInCompositionMode) {
return;
}
switch (e.getKeyCode()) {//处理左右移动键,将提交CARET_POSITION_CHANGED
case KeyEvent.VK_LEFT:
moveCaretLeft();
break;
case KeyEvent.VK_RIGHT:
moveCaretRight();
break;
}
} elseif (eventID == KeyEvent.KEY_TYPED) {
char c = e.getKeyChar();
if (notInCompositionMode) {
if (c != '""') {//如果当前没有在拼写状态,忽略该事件
return;
}
startComposition();//但是如果输入的是'""',进入拼写状态,并提交 //INPUT_METHOD_TEXT_CHANGED
} switch (c) {
case' '://输入空格则要求转换unicode编码,生成对应unicode字符,并将此unicode字符提交
//INPUT_METHOD_TEXT_CHANGED
finishComposition();
break;
case'"u007f': //输入DEL键则删除当前编码字符并提交 //INPUT_METHOD_TEXT_CHANGED
deleteCharacter();
break;
case'"b': //输入BACKSPACE键则删除前一个编码字符并提交 //INPUT_METHOD_TEXT_CHANGED
deletePreviousCharacter();
break;
case'"u001b': // 输入Escape键清空编码字符并提交 //INPUT_METHOD_TEXT_CHANGED
cancelComposition();
break;
case'"n': //输入回车键
case'"t': //输入TAB键发送当前所有编码字符并提交//INPUT_METHOD_TEXT_CHANGED
sendCommittedText();
break;
default:
composeUnicodeEscape(c); //按unicode规则接受当前输入字符作为编码字符并提交 //INPUT_METHOD_TEXT_CHANGED
break;
}
}
} else {
//如果当前没有在拼写状态,忽略KEY_RELEASED事件
if (notInCompositionMode) {
return;
}
}
//没有忽略的事件都将被consume掉。
a. consume();
CodePointInputMethod处理逻辑概括地说基本上为维持一个StringBuffer buffer,int insertionPoint ,在active时进行初始化。此后当dispatchEvent时,首先判断buffer是否为空,为空则期待输入'""',如果输入的不是'""'则直接忽略过去(被忽略的事件将继续进入组件的keylistener得到处理);如果是'""'则存入buffer,并insertionPoint++,然后以INPUT_METHOD_TEXT_CHANGED提交给InputMethodContext---即将buffer包装成AttributedString,并且置属性INPUT_METHOD_HIGHLIGHT,最后就会调用InputMethodContext. dispatchInputMethodEvent,在其中包装成InputMethodEvent进行分支发送事件。那么如果buffer不为空,则说明已经开始收集unicode编码了,这时将期待收到合理的unicode编码字符,因此对每一个收到的字符进行合理性检查,合理的将和上述'""'一样处理,不合理的会通过Toolkit让机器发出”哔”的一声提醒就拉倒(不会再传给keylistener),除此期待,还会考虑那些Del,BackSpace,空格,回车等控制符,比如Del将会删掉buffer在insertionPoint的字符,然后提交InputMethodContext,至于空格,则是要求对buffer的unicode编码进行转换,得到对应的字符codePoint,覆盖buffer的现有内容后再提交InputMethodContext。至于左右键,则会引起insertionPoint的增减,并且以CARET_POSITION_CHANGED提交给InputMethodContext。
CodePointInputMethod提交InputMethodContext后的处理将进行分支,对Active-below-the-spot以及passive将经过context.getCompositionAreaHandler(true)(延迟创建单例)来处理,其处理逻辑为:首先根据ComposedText和carset在CompositionArea画出来,如果有CommittedText,将会再次inputMethodContext.dispatchCommittedText,其将继续根据Active-below-the-spot以及passive分支,前者产生InputMethodEvent提交给组件,后者产生KeyEvent提交给组件。最后,各类组件的对应listener里根据监听到的事件的信息来更新私有成员text等并做重画处理。
注意:CodePointInputMethod是一个很简单的输入法,不需要打开什么文件资源,也没有打开任何窗口界面,只需实现dispatchEvent方法就OK,顶多补充了一些基本的方法实现,比如isCompositionEnabled恒返回true来告诉输入上下文将一直支持解析。如果我们要实现更复杂的输入法,可借助InputMethodContext.createInputMethodWindow或createInputMethodJFrame建立窗口界面,通过对InputMethodContext.enableClientWindowNotification来获得客户组件窗口的变化通知,然后在inputmethod里要在active,deactive,hideWindows,removenotify,dispose等方法来实现资源获取释放的逻辑,而在notifyClientWindowChange方法里响应客户组件窗口的变化。如果需要更多支持,reconvert()用来支持文本组件回转拼写,getControlObject()用来支持对输入法的设置。
d.输入事件归集到组件上,组件如果enableInputMethod,getInputContext将负责下一步处理,InputContext处理过程中要利用当前指定InputMethod进行协调处理,最后将InputMethod对输入事件转换拼写输入后的处理结果-纯碎的文本输入结果再交给组件的监听进行下一步处理。 InputContext还要维系当前InputMethod,并在发生焦点转移,请求输入法切换的时候处理好相关事宜。具体过程为:
InputContext在构造时即通过InputMethodManager.getDefaultKeyboardLocale获取系统默认locale并据此locale完成初始输入法的选择。选择过程主要委托给InputMethodManager.findInputMethod(Locale locale),在那里将首先根据用户preference来查找,然后将从本地系统输入法中查找,最后才从java输入法找出支持此locale的InputMethodLocator。当找到合适的InputMethodLocator后将设置该输入法为当前输入法。设置动作通过InputContext.changeInputMethod方法中完成。changeInputMethod将首先判断当前inputMethodLocator是否为空(在初始化的情况肯定是空的),若为空则只是赋值即可返回。而当输入法切换菜单被点击时,菜单actionPerform将再次调用InputMethodManager.changeInputMethod方法,经由InputMethodManager记录此作为用户preference输入法后,转到当前InputContext. changeInputMethod,此时InputContext.inputMethodLocator将是上一次的输入法,需要判断待切换的输入法是否和旧输入法相同,如果相同需要替换成新的locator,再,如果旧inputmethod实例已经存在,则将此输入法重置状态;以上都不满足的情况下,即旧的输入法是不同的输入法,将需要清理旧输入法,替换为新输入法。
注意:InputContext总是要尽量延迟inputmethod实例的创建,因为inputmethod实例的创建将可能是一个耗时操作,所以在定下来要用某一个输入法作为当前输入法时,首先考察这个输入法和上一个输入法是否是同一种输入法以便重复利用;即使是新的输入法也只维持一个inputmethodlocator,只有当需要激活此输入法时,才要去创建inputmethod的实例,而即使这时的创建也首先从usedInputMethods缓存中查找出来,只有缓存中不存在的情况才去通过descriptor.createInputMethod去创建实例(并马上setCharacterSubsets)并以后会缓存下来。
如果当前组件失去焦点,该focus_lost事件会流转到inputcontext中进行处理,处理逻辑主要是deactivateInputMethod, setCompositionAreaVisible(false)等;
如果当前组件获得焦点, 该focus_gained事件会流转到inputcontext中进行处理,处理逻辑主要是将将输入法转换输入提交给上一个组件,activeMethod,并根据情况setCompositionAreaVisible(true)等;
注意:InputContext在上述切换,焦点变化的过程中要处理好三个方面:一个是属性延续的效果,即本输入法要尽量沿袭上一个输入法的locale,active, CompositionEnabled等属性,并要应用历史的clientWindowNotificationEnabled属性;一个是既然多窗口多InputContext,尽量达到窗口切换各不影响输入法的效果;另一个是清理上一个输入法要彻底,包括endComposition,通知deactivateInputMethod,setClientComponent(null) , 缓存此输入法,并保存clientWindowNotificationEnabled属性,hideWindows等等.