Fcitx 5 开发(一)一个按键事件的前半生:由程序到输入法

因为觉得有必要趁着 Fcitx 5 发布的功夫,决定写一系列的关于如何开发 Fcitx 5 相关内容的文档,之后会更新到 Wiki 上。

要理解输入法是如何工作的,首先要了解的是程序究竟是如何与输入法进行交互的。而这一部分的内容,也与一个常见问题相关:为什么我在 A 程序里面无法使用输入法?

Fcitx 的基本架构是客户端/服务器架构,客户端,在 Fcitx 里面对应的词是“输入上下文(Input Context)”,具体究竟对应程序中的哪一个实体,是并没有明确规定的。一般来说,可能是对应程序的一个窗口。而具体对应什么,是取决于程序自己的实现,也就是“程序认为”哪些部分应当作为同一个上下文来处理。虽然,在实践当中一般并不会有程序做到如此的细化。

当一个程序不能用输入法的时候,一般最常见的情况就是这个程序并没有采用正确的方式和输入法进行通信。

那么,目前在 Linux 下到底有多少种不同的客户端通信方式呢?看看源码中 frontend 目录下有多少就知道了。

这里我们也详细说明一下各个目录的情况:

1、xim,最古老的基于 X11 的输入法协议。在 X11 下理论上是通用的,实现由 Xlib 完成。和 Xlib 的事件处理结合非常紧密,采用了一种直接原生和 Xlib 按键转化字符串的方式来实现,而不是非常直接的有回调来传递输入法想要程序进行的操作。Xlib 的实现里面非常多的操作都是实现成同步而且并没有错误处理,这也导致了一旦输入法退出,程序如果此时在进行一个同步操作,就会直接冻结。而这部分代码过于古老基本上处于没人会去改的状态,其中非常古老的缺陷之一就是光标跟随和嵌入预编辑文本无法共存,尽管从协议的角度来说,并没有什么道理阻止这个功能,但就是几年尽管有 Patch 都没有合并的状态。

XIM 允许多个输入法注册在 Root Window 上,具体使用哪个可以通过 XMODIFIERS 这个环境变量设置。Fcitx 的做法是,自己也读取这个环境变量,如果读取到了,就根据这个环境变量的值来选择要注册的名字,因此通常设置的 XMODIFIERS=@im=fcitx 这里的“fcitx”是什么并不重要,因为 Fcitx 自己反正会尝试设置和系统值一致的。

而目前很多界面都采取了一种直接去除 XIM 的态度。Qt 5 和 SDL 2 是最早去除的,而主要的原因是他们都切换到了 xcb 来处理 X11 的通信。尽管 Xlib 现在内部已经变成了基于 xcb 的实现,但是,如果原生就是 xcb 的情况,是无法反过来再用 xlib 的。而在当时,并没有一个基于纯 xcb 的 XIM 实现。我在开发 Fcitx 5 的过程中,开发了一个纯 xcb 的 XIM 实现。当然虽然现在 XIM + xcb 已经又成为可能,但是大概也没人会把他再添加回来吧。可能有一些纯 xcb 的程序也许会考虑用它来支持 XIM 输入。

2、dbusfrontend,这个就是 Fcitx 自己的协议了,基于 dbus 实现的。需要程序配合使用对应的 DBus 接口,主要就是通过 IM Module。而这个主要就是通过常见的两板斧:GTK_IM_MODULE 和 QT_IM_MODULE 环境变量控制。

事实上,即使你不设置环境变量,Gtk/Qt 也会自动挑选,但是就不一定挑选到哪个了,可能根据系统语言和文件系统遍历文件顺序选,但往往不对,因此都是要求用户设置这两个环境变量来保证输入法的使用。另外,环境变量也不止这两个:

QT4_IM_MODULE:顾名思义,就是只给 Qt 4 的程序设置的环境变量。
SDL_IM_MODULE:给 SDL 2 程序设置的
GLFW_IM_MODULE:kitty 这个终端模拟器自己发明的…目前只有一个 ibus 可以作为值设置。

在从 Fcitx 4 迁移到 Fcitx 5 的过程中,考虑到用户也许不一定要使用某个 im module,因此在同时需要支持 Flatpak 的背景下,新加了一个和 Fcitx 4 原有协议类似但不完全一样的接口(消除一些过时的接口),同时使用通用的为 flatpak 准备的 DBus 服务名。这样来让 Fcitx 4 / 5 的可以任意搭配 Fcitx 4/5 的 IM Module 进行输入。

3、fcitx4frontend 和 ibusfrontend,顾名思义,就是模拟原本 Fcitx 4 自己的 IM Module 协议和模拟 IBus 的。这里主要是出于兼容性的考虑,因此添加了 IBus 的模拟支持,意外发现这个决定还是相当正确的,因为 GNOME-Shell 自己它砍掉了 Gtk 的 IM Module 部分而直接采用了 IBus ,因此在 GNOME-Shell 的搜索框就必须用 Fcitx 5 才能正常输入。另一方面,因为 Qt 5 移除了 XIM 而内置了 IBus 的支持,兼容 IBus 也能提供对那些只 Bundle 了 IBus 的闭源 Qt 程序的支持。

而 Fcitx 4 的 Frontend 主要是出于对那些闭源又只包括了 Fcitx 4 IM Module 的程序来说的。让他们一个个都更新到 Fcitx 4 较新的 IM Module 实在不太现实,特别是上游经常反馈了也毫无动作。

4、waylandim,实现了 zwp_input_method_v1 和 zwp_input_method_v2 的支持。但不幸的是,没有什么正经 Compositor 支持。而且这两个协议对于输入法上下文都缺少跟踪能力。V1 在程序失去焦点之后就销毁上下文,V2 则根本就没有上下文的概念。因此我时常说,这两个协议反正支持了也没什么大用。而在 Wayland 里程序是使用 text-input 协议和 Compositor 通信,Compositor 再中转成 zwp_input_method ,问题就是 text-input 程序本身支持就缺乏,结果反过来就更没有程序能正常使用这个了。

有了客户端之后,客户端和 Fcitx 之间最重要的,就是按键了。在使用不同的 Frontend 的前提下,按键尽管到达输入法的途径并不相同,但是总归是输入法接受按键,处理按键,发送需要程序进行的操作这样的流程。

接下来重新针对每个 Frontend 讲述一下按键事件到达输入法的区别。

1、XIM:具体的实现细节通过 XFilterEvent 函数隐藏起来了。这里的行为是先由显示服务器(Xorg)将事件传递给程序,在 XFilterEvent 中根据目前的情况(焦点等),将按键包装在 ClientMessage 当中(另一种 X 事件类型,内容载荷可以当作纯 binary),发送给输入法。

2、对于 dbus/fcitx4/ibusfrontend ,具体的传输都是由 DBus 进行的,和 XIM 类似,程序接收到来自显示服务器(Xorg / Wayland)的按键事件之后,通过调用 IM Module 这一层抽象,IM Module 再调用对应的 DBus API 来将按键事件传输给 Fcitx。如果没有选择正确的 IM Module,则自然不会将按键发送给输入法。这也正是为什么输入法有问题时,多数情况是检查环境变量设置是否正确。

3、Wayland 则更特别一些,zwp_input_method_v1 和 XIM 类似,程序通过 text input 协议发送按键,但不是直接传送给输入法,在 Wayland 下也无法做到,中间通过 Wayland Compositor 进行中转,再由 Wayland Compositor 利用 zwp_input_method_v1 传输给输入法。zwp_input_method_v2 则不同,由输入法在需要的情况请求一个 Keyboard grab,请求之后,Compositor 则不会将按键发送给程序,而是直接发送给输入法。

但无论是哪种方式,接收到按键才能让输入法正常工作,不同的方式有一些不同的与按键事件传递不同的能力,例如是否能获取程序是什么,但其他方面则没有什么太大不同。

按键从你键盘按下之后终于历经千辛万苦终于到达了 Fcitx,Fcitx 终于可以开始处理按键了。下一篇将会主要介绍按键在 Fcitx 内部的处理过程。

This entry was posted in fcitx development. Bookmark the permalink.

One Response to Fcitx 5 开发(一)一个按键事件的前半生:由程序到输入法

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.