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 内部的处理过程。

Posted in fcitx development | 1 Comment

Client Side Input panel for Fcitx 5

(English version is at the end of this post.)

输入框的定位相关的问题其实很复杂,过去 Fcitx 总是自己绘制相关的窗口。X11 这个办法还好使,但是其他情况可能就没这么好使了。让客户端绘制窗口的想法其实比 Fcitx 5 要早得多,最早是为了 fbterm 而增加的支持。

显然 fbterm 没法利用任何来自显示管理器的界面,所以它需要自己绘制它自己的界面。那会儿我添加了一个很简单的实现来支持这个功能。

为了正确显示界面需要支持以下功能:

  • 窗口显示在光标的位置
  • 窗口不能移出屏幕

在 wayland 里,和 X 最大的区别就是不知道窗口的位置,所以不得不使用特别的方法来支持。

  • 用 wayland 的输入法协议的话,它确实是支持将某个窗口标记为输入法窗口并让混成器帮你进行定位,但是需要程序自己先使用 wayland 的输入法协议,混成器也要支持这个协议。目前这两部分的支持并不好。
  • 或者让窗口传送一个相对坐标,然后让混成器帮忙移动窗口。gnome wayland + kimpanel 的扩展就是这样进行定位的,因为 kimpanel 扩展正好是运行在混成器里面所以知道所有的窗口信息。但是让各种混成器都支持这个也不太现实。
  • 然后就是本次说到的方式,让程序自己绘制界面。当然,缺点就是实现通过 im module 的话,就只能支持 Gtk / Qt 的程序,但相对来说可以覆盖大部分的程序了。我个人对于这个方法比较反感,因为感觉可能出问题的地方很多,对于程序来说添加一个额外窗口和过去的 im module 相比有了更强的侵入性。不过在其他很多系统上,例如 windows 和 uim 可能都使用了这个方式。

不过相比只是因为担心就不采取行动的话,还不如先试一试,毕竟解决实际存在的问题的意义更加重大。想要测试的话需要使用 fcitx5 / fcitx5-qt / fcitx5-gtk 的 git代码。

目前还有一些实现的限制:

Gtk 3 不支持已经实现的窗口重新移动位置,Gtk 4 对于不支持较新的 xdg popup positioner 的混成器的是采用隐藏再显示窗口的方式,而这个方式会导致窗口闪烁(Qt 用同样的 hack 就不会,我猜测是 Gtk 有和刷新率对齐之类的操作?)。所以我干脆就没在 Gtk 3 用这个 hack,毕竟闪烁起来看着更糟一点…

Qt 的话出于没有暴露 xdg positioner api 的限制,在窗口定位方面并不能保证不移出屏幕,因此我采取了另一个方式:保证输入框总是在窗口范围内。这个对于小窗口并不是特别友好,例如 krunner,不过我也没有更好的办法了。

本文就是用 firefox + wayland 输入的,整体来说还行……Gtk 3 定位延迟虽然有点难受不过反正我也没更好办法啦。

In the past years, fcitx always renders its pop up on by itself. This is Ok for X11, but not so good for other cases. The idea of letting client to render its UI actually existed long before fcitx 5 comes out. It was first introduced in fcitx 4 for fbterm.

Obviously, fbterm can not render UI using any display server, so it need to render the UI by itself. At that time I added an adhoc implementation for it to display a UI with a minimum implmentation.

In order to properly display an input window, there are certain features need to be supported:

  • Input window should be positioned right beside the cursor poistion.
  • Input window should not be placed out of screen.

In the wayland world, things is also very different from X. Without the knowledge of window positioning, we need to have some different way to support this.

There are current multiple approaches being used with in Fcitx:

  • With zwp_input_method, the wayland protocol, it has support to declare a surface to be input panel window, and compositor will position it properly. Unfortunately, this requires client to use wayland and text-input protocol, which is not yet well supported by the clients.
  • Or, with im module, send over a relative coordinates and ask composition to position the window with this “relative” coordinates instead of traditional absolute ones. This is used by gnome wayland + kimpanel extension. Luckily, gnome-shell extension runs within compositor, which means it has all information about the windows, e.g. which window currently has focus, window position. It can also to freely move the window created by extension. But this won’t scale anyway, because it’s unlikely to make all the compositors to support the same thing.
  • Then we can still ask client to render the window, just like fcitx-fbterm. This is an intrusive way for the application, so I never tried this approach before. Just FYI, this approach is being used by many other systems, for example, Windows, So it is not an rare solution. I also heard that uim is using such approach. But it also has its downside: duplication of code for rendering, inconsistency between toolkits, only support Gtk/Qt, more likely to have problem for certain weird application.

There is no silver bullet to this problem, but I always want to help our users to type text more freely, regardless of which desktop they want to use.

So free free to give it a try, you’ll need git from fcitx5 / fcitx5-gtk / fcitx5-qt. So far I tested many applications, including libreoffce, firefox that I suspect may have problem, but so far it looks good.

There still some limitation in this implementation:

  1. Gtk 3 doesn’t support xdg popup reposition, while this is not yet supported by a lot of compositor, Gtk 4 workaround this automatically by show and hide the window. But if I use such workaround manually in Gtk 3, it will cause the window flicker (Similar code in Qt won’t, which is really weird. My guess is this is related to frame alignment or sth.).
  2. Qt doesn’t expose all the possible interface for xdg popup, which means the position can’t use the slide / flip parameter like Gtk to ensure it is within the screen. So I took another approach here: ensure the popup window always within the range of original window. This doesn’t work well for tiny window like krunner, but I have no other good solution for it. Qt 6’s wayland is able to get the native interface for xdg popup, so it might be possible to do some manual coding there.
Posted in fcitx development | Tagged , , , | Leave a comment

构建一个给 WPS 用的 fcitx im module

WPS 处于许可证的考虑,里面自带的 fcitx 的 im module 是由 WPS 的开发自己写的。但是这就有一个问题了,他们对于跟随 fcitx 上游的变化可以说是几乎没有。在 Fcitx 4 已经加入了对于 flatpak / fcitx 5 的支持之后他们并没有做出对应的改变,导致了现在的很多问题。

在过去的这篇也曾经写到了,如何构建一个给闭源 Qt 程序用的模块。

我不知道过去有没有人干过这样的事情,但我们现在就来做这么一趟。

首先观察 WPS 使用的 Qt 版本

$ ls /usr/lib/office6/libQtCore.so -l                       
lrwxrwxrwx 1 root root 18  1月 15 14:23 /usr/lib/office6/libQtCore.so -> libQtCore.so.4.7.4

显而易见,是 Qt 4.7.4,出于 Qt API 的修改可能有兼容性的考虑,我们先去下载一下 4.7.4 的源代码。现在可能面临两个问题:

1、Qt 4.7.4 的代码在当前系统上可能编译不了了

2、WPS 的 Qt 可能修改过大。

当然 2 的概率相对是较小的,即使内部的修改也不太可能影响到 API。但 1 确实是有这样的现实的问题。而且在这次也确实遇见了,但是我们其实不需要大部分的工具,我们需要的只是 moc,qmake 这两个工具,即使其他都编译失败也没有关系,因为 WPS 里面已经有了对应的编译好的库。

下载源代码之后,编辑 configure 文件,设置 CFG_INOTIFY=no ,执行

 ./configure -opensource -confirm-license -no-openssl
make

当然确实是出现了一些奇怪的错误,不管他。我们其实需要的只是生成的 qtconfig.h 文件和 qmake 和 moc。

然后准备一些 fcitx 的源文件,用 fcitx 或者 fcitx5-qt 都可以,早前我已经基本让 fcitx 4 的 im module 脱离需要链接 fcitx 本身的库,只需要链接 Qt 即可。但是我还是引用了一些 fcitx 的头文件。用 fcitx 4 的代码就需要手动复制一些 enum 的定义过来。因此 fcitx5-qt 可能更好一点。

我特意修改了一下 fcitx5-qt 的代码让它可以不修改就能编译通过。写了一个专门的 cmake 配置,我也就是随意一写,所以放到了个人帐号上。 https://github.com/wengxt/fcitx5-wps 。利用 git submodule 直接引用了 fcitx5-qt 的代码,所以 clone 之后别忘了 git submodule update –init

具体编译参数就需要设置 -DQT_QMAKE_EXECUTABLE=<qt源代码路径>/bin/qmake 还有 -DWPS_LIBRARY_DIR=<WPS 的 qt 库路径>

然后生成了库之后把他复制到 WPS 对应目录 /usr/lib/office6/qt/plugins/inputmethods/qtim-fcitx.so 测试运行。当然,最好删掉原来的那个 libqim-fcitx.so 文件,避免环境变量设置成 fcitx 时会读到这个。

嗯?为什么不行?这时候就要利用 Qt 的一个调试用环境变量来测试了

QT_DEBUG_PLUGINS=1 /usr/lib/office6/wps

可以发现有如下输出:

QFactoryLoader::QFactoryLoader() looking at "/usr/lib/office6/qt/plugins/inputmethods/qtim-fcitx.so"  
In /usr/lib/office6/qt/plugins/inputmethods/qtim-fcitx.so:
 Plugin uses incompatible Qt library
 expected build key "x86_64 linux clang full-config", got "x86_64 linux g++-10.2.0 full-config"
"The plugin '/usr/lib/office6/qt/plugins/inputmethods/qtim-fcitx.so' uses incompatible Qt library. Expected build key "x86_64 linux clang full-config", got "x86_6
4 linux g++-10.2.0 full-config""  
        not a plugin

哦,QT_BUILD_KEY 对不上号呗。好办,直接强行修改生成的对应 qconfig 文件的内容。

在 Qt 源代码里面找到 src/corelib/global/qconfig.h ,把 QT_BUILD_KEY 修改成 WPS 对应的。这部分是直接构建在 plugin 里面的,所以重新编译。测试输入,成功。

Posted in fcitx development | Leave a comment

仿原神 UI 的 KDE 系统配色

原神痴竟是我自己

下载地址:https://uploads.csslayer.info/uploads/Genshin.colors

Posted in Linux | Tagged , , | Leave a comment

如何现在就用上 Fcitx 5 (Flatpak)

折腾了很久的自动构建,现在终于可以拿出来给大家用一下了。当然,据说 flathub 有特色网络问题……这部分我就不负责解决了。

首先添加源:

# 添加 flathub 到用户级
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
# 添加 fcitx5-unstable 到用户级配置
flatpak remote-add --user --if-not-exists fcitx5-unstable https://flatpak.fcitx-im.org/unstable-repo/fcitx5-unstable.flatpakrepo

安装 Fcitx 5 和输入法:

flatpak install org.fcitx.Fcitx5
flatpak install org.fcitx.Fcitx5.Addon.ChineseAddons
# 例如需要 Rime: flatpak install org.fcitx.Fcitx5.Addon.Rime
# 可以用 flatpak remote-ls fcitx5-unstable 查看有哪些包

然后,在本地非 flatpak 程序上使用的话,还需要安装 fcitx 4/5 的 im module。这个 repo 除了提供另一种更新方式之外,也主要是为了方便还没提供 fcitx 5 的发行版测试。fcitx 4 只要是最近的版本,都可以直接和 fcitx 5 配合使用。环境变量方面当然还是和过去一样,比较传统的就是设置在 ~/.xprofile 里面(和过去一样,如果你已经对 fcitx 4 进行设置了就不用修改什么。)

export QT_IM_MODULE=fcitx
export GTK_IM_MODULE=fcitx
export XMODIFIERS=@im=fcitx

自动启动就把 flatpak 的 fcitx 5 的 desktop file 加入到自动启动即可。但相对的也需要阻止 fcitx 4 的自动启动。如果没有拆包而导致 fcitx 被安装,如果系统提供了 /etc/xdg/autostart/fcitx-autostart.desktop 就需要在 ~/.config/autostart/fcitx-autostart.desktop 新建如下内容的文件

[Desktop Entry]
Hidden=true

如果是采用 im-config 等类似的方式,则直接禁用并自行配置环境变量即可。

目前仅仅是测试版本,可能会遇见各种问题(例如图标缺失等),如果在用 flatpak 版本的时候有任何问题请汇报到 https://github.com/fcitx/flatpak-fcitx5/ 。

Posted in fcitx development | Tagged , , | Leave a comment