目录
目录 3
一、问题描述 4
二、原因分析 4
1、系统排查 4
2、输入法排查 4
3、XKB排查 6
4、确认窗口管理器 6
三、可能的方案及验证实施 7
四、所需的背景知识 8
1、X 和 XKB 8
术语说明 8
配置文件目录 9
Xkb的 level 和 group 11
修改XKB配置 setxkbmap 11
2、X 相关的接口 12
3、Mutter中如何监听 Xorg 事件 13
Glib中的事件处理 13
五、小结 14
一、问题描述
输入法-五笔字型、五笔拼音、拼音输入法的键盘布局设置未生效
[步骤]:
1、将五笔字型配置界面的“键盘布局”设为“colemak”,点击“确定”。
2、打开文本文档,将输入法切换到“五笔字型”,输入字符。
[预期结果]:
输入的字符遵从设置的新布局
[实际结果]:
输入的字符遵从默认布局
二、原因分析
1、系统排查
这个问题是去年五月份报出来的,输入法的测试对比情况是:cbs上下载最新版本的输入法在2.A上编译安装可用,但是在2.2.005版本不可用,于是归结于系统原因。2.A的版本是去年年初的刚升级新基线的版本,在它之后的版本做了许多包的升级。比如 2.A 自带的是 Cinnamon 桌面,之后的版本就换成了大T桌面,也就是说,窗口管理器从 muffin 换成了 mutter。不过一开始也没意识到窗口管理器跟这个问题有什么关系,所以都认为应该是系统的问题。
经过系统组排查,他们在终端启动一个新的X,比如 :1,在新启的X上使用输入法打字,键盘布局是能生效的,所以基本排除掉系统原因。
2、输入法排查
桌面这边尝试不启动桌面程序,只启动窗口管理器mutter,还是复现这个问题,换成 metacity 就不再复现了,所以定位到问题在于窗口管理器。但是从窗口管理器开始分析问题目标太庞大,所以一开始还是需要从输入法入口,再分析出原因。
入手时尝试在fcitx和 fcitx-config-gtk3 中打印信息,用来定位当用户选择某一键盘布局的时候,实际调用的接口在哪里?找到了接口才能进一步缩小范围。
首先fcitx-config-gtk3工具源码包为 fcitx-configtool,源码文件 gtk3/im_config_dialog.c,接口函数 _fcitx_im_config_dialog_response_cb,打印出的layout、variant 的值都是对的。排除这个工具的问题;
这个工具调用的设置接口fcitx_kbd_set_layout_for_im(在 fcitx 源码中定义),实际上就是发一个 dbus 调用,call 的方法是 SetLayoutForIM,在 fcitx 中要找到这个 dbus call 的 handle 函数。根据 dbus 的 method 找处理函数有两种方式,如果 dbus 框架是自动生成的,那么在方法名前面加 handle- 找信号;如果 dbus 框架是自己写的,直接搜关键字 SetLayoutForIM 就可以了。Fcitx 中属于后一种,定位到文件 module/xkbdbus/xkbdbus.c ,函数名 FcitxXkbDBusEventHandler, 接下来找具体的处理函数,定位到文件 src/modules/xkb/xkb.c,接口FcitxXkbSetLayoutOverride。一步步追踪下去,其实实际设置键盘布局的接口函数为FcitxXkbSetLayout,接收参数layouts、variants 和 options。在这个函数中,它会找到 rules 文件,FcitxXkbGetRulesName,将用户设置的这几个参数整理并写到对应的 xml 文件中[xml 文件参见第四节背景知识]一直到这里,数据传递什么的都没有问题。
打印log如下:
FcitxXkbUpdateProperties打印的 rules 文件为 evdev,model 为 pc105 cn, layout 为 us, variant 为 cokemak,都是用户设置的值。
XChangeProperty通过这个接口将所有信息设置到 XDefaultRootWindow的属性中去。跟踪到这里都还没有出问题,那就是 X 这块的问题了,要知道键盘布局的设置有个 X 扩展 XKB 来负责。所以接下来需要查找 XKB 的相关资料。
FcitxXkbUpdateProperties 423: 50, atom name: _XKB_RULES_NAMES, pval: evdev
根据打印的信息来看,设置的属性为_XKB_RULES_NAMES,需要去 xkb 中看看修改这个属性后发生了什么?
3、XKB排查
在XKB的概念中,从一个键盘布局切换到另一个键盘布局,其实是 groups 之间的切换,每个 group 是一个逻辑概念,表示相同字符集的字符组成的一个组。当用户通过快捷键或者应用程序通过接口调用操作切换到其他组,实际上就是去这个组里去找对应的应该显示出来的字符。
这个bug切换键盘布局不起作用,其实就是切换group 的操作没有起作用。这里需要介绍一下 xkb 的 keyboard state 的概念。X core 与 XKB 扩展加起来一共有两种 keyboard state:
1) Core:定义了 8 个修饰符 ( Shift , Lock , Control , and Mod1 - Mod5 )
2) XKB 扩展:在核心协议外,扩展了四个 keysym groups
为了方便开发,XKB对外提供简单的接口用来 lock 和 latch 操作,锁住的操作对象就是 mdifiers 和 groups,这里的 lock 和 latch 都有锁住的意思,区别在于一个是长期的锁住,一个是临时的锁住。如果调用 XkbLockGroup 之后,之后的 key 按键都会解析成这个 group 中对应的字符,除非调用 lock 到其他组。而 latch 只会影响到下一个 key event,并且这个操作不会触发 keyboard state 的 stateNotify 信号。
根据这些信息可以判断,当用户在输入法配置工具中选择其他的键盘布局,会触发XkbStateNotify信号,由于 muffin 没有问题,说明 X 、X 扩展这部分我们不用关心,肯定没问题。出问题的是 mutter,也就是说看看 mutter 中关于这个信号有没有做特别的处理。
[if !supportLists]4、[endif]确认窗口管理器
窗口管理器中监听Xserver连接的 G_IO_IN 事件,拦截 xkb 信号并处理。XPending 函数返回从 X server 接收到的 events 数目(这些 event 还处于 event queue 中)。根据 XPending 的数值决定调度的优先级。
如果我们修改了键盘布局,每次切换到拼音输入法的窗口,都会触发信号XkbStateNotify,mutter 拦截到这个信号后,首先判断 mutter 这边的 locked_group 与 xkb state的 lock group 是否一致(根据打印出来的信息看,mutter priv lock: 0, xkb state lock: 1)。如果不一致,就锁住mutter 自己的 locked_group。锁住 group 的意思是,停留在 mutter 自己设定字符集 group 中,不允许 xkb 切换到其他 group,所以我们修改键盘布局不生效,因为被 mutter 锁住了不让切。
关于group的定义:group 是键盘的逻辑状态,通过切换 group,可以切换到其他字符集。同一类字符集我们逻辑上把它们归为一组,这个组内可能通过 shift 可以切换到不同的 level。见背景知识——Xkb 的 level 和 group。
Mutter中的代码:src/backends/x11/meta-backend-x11.c ,处理函数为handle_host_xevent。关于这个回调函数的触发,见背景知识—— Mutter 如何监听 Xorg 事件。
三、可能的方案及验证实施
这个问题是很久之前mutter自己为了修改缺陷改出来的,所以方案就是需要修改 mutter。查阅 mutter 的修改记录,关于 handle_host_xevent 这部分的修改对应一个 bug (Bug 756543 - bug in modifiers-only input source switching?)。Bug 链接见附录。
这个问题来源与X server的某一个补丁,在这个补丁中,X server 切换组的快捷键时,响应 key release 事件。而 Mutter 在 key press 事件发生时就拿到了 XKB_KEY_ISO_Next_Group,并且执行了XLockGroup()。 X server 在 key release 事件时给 group index 又加一。也就是说实际上切换的 keyboard group 不对。
这是一个数据不一致的问题,为了解决这个问题,mutter拦截 XkbStateNotify 信号,并且判断如果请求的 locked_group 不是 mutter 自己需要的,就直接 lock mutter 自己维护的 locked_group 值。这样做解决了 Bug 756543,导致的问题就是我们切换键盘布局也被它拦截掉了。切不到我们需要的 group(比如colemak),不管怎么切换都切换到 mutter 自己认定的 group。
不过由于X server的那个补丁取消了,mutter 为了适配这个问题所做的修改也可以直接去掉。根据 mutter 的git 记录来看,2016年 5 月21号提交这个修改。2017 年 1 月 31 号取消。我们基础版升级新基线选取的版本是 2016 年 12 月 6 号。正式这样的时间差导致存在这个问题,将这里关于 XkbStateNotify 的拦截取消修改就可以了。
四、所需的背景知识
1、X 和 XKB
对于大多数系统来说,有了X才能显示图形化程序,很多资料都说默认配置 /etc/X11/xorg.conf,这个文件包括键盘配置的信息。但是现在的目录中已经不存在这个文件了,键盘配置的话主要在:
/etc/default/keyboard
/usr/share/x11/xkb
对于第一个文件,X服务从这些配置文件中读取到配置信息,传递给 XKB, XKB 其实是 X 的一个扩展,专门用来处理键盘的设置。 Model、layout、variant 等。
对于第二个目录,这个是给窗口管理器用的,对我们的系统来说,也就是mutter从这个目录中读取各种数据信息,给它的键盘管理程序来用。
图中选择布局列表,就是从这个目录下读取的信息。
术语说明
home row:表示打字前,我们手指头们放置的那一行。
键盘布局:键盘上的key是怎么安排的。
QWERTY是我们最常用的一种布局。名字很简单,就是键盘上显示的样子。
DVORAK也是一种布局,它的名字就是发明这个布局的人名。这种布局最主要的特征就是所有的元音都放在 home row 的左边,提升打字速度,手指免得动来动去那么累[元音最常用?]asdfg 被映射为 aoeui
还有好多其他的布局,比如colemak等等。
layout variant:为了适应不同的语言。比如 English 和 Russian 的字符集不一样,再比如英语和意大利语有相同的 latin 字符集,但是 English 相对 Italian 来说还缺少一些变音符号。
Key mapping
Key-code ,命令 xev -event keyboard 可以看到 key press 和 key release event 以及对应的 key code。Key code 位于 xkb 更下层,是键盘发出来的码,表示物理键盘上哪个键被按到了。
Modifiers key
对于普通的key,我们在执行 xev 命令的时候长按不放,key press 和 key release 事件会不停的刷,modifiers key 就不会这样。«CTRL», «SHIFT», «ALT», «ALT_GR», «ESC», «CAPS LOCK«, «INSERT», «NUM LOCK» 这几个键就是 modifiers key。
修饰父的作用就是暂时修改普通字符,比如按住a的同时 shift 键不放就可以打印大写字母。
配置文件目录
/usr/share/x11/xkb这个目录下有好几个目录,根据作用不同大概可以分为三类:
第一类就是图形化显示键盘布局的配置文件,geometry目录下的文件,键盘管理器读取它来显示键盘外观。cinnamon keyboard 命令打开键盘配置,选择键盘布局打开,正常情况下显示如下:
对应的文件为geometry目录下的 pc,我们的键盘经过调试打印出来的信息是 pc105, 如果将 pc105 的内容删掉大部分,再打开这个键盘布局,显示如图:
第二类是配置键映射与键盘布局的文件
keycodes和 symbols 目录下的文件用来设计自定义布局。keycodes/evdev 文件将 keycode 和 keysymbol 映射起来。XKB 读取 keysymbol 会根据 symbols/us 中的映射表解释它。
比如keycodes/evdev文件中:
symbols/us中:
key
key
TLDE绑定到两个字符 ` 和 ~
BKSP绑定到 «Backspace» 和 «Backspace»
后面的俩字符通过shift + key来实现切换。` 通过 shift 切换为 ~,对于 Backspace,不管有没有 shift, «Backspace» 就是 «Backspace»。
symbols/us里面包括多种布局,每种布局都可以自己从头写,当然更方便的做法是继承其他一个布局自己做点儿修改。
第三类是启用配置,包含一些规则,XKB通过读取这些文件知道系统当前可用的布局,以及布局的子节点(layout-variants)
base.lst
base.xml
evdev.lst
evdev.xml
base.lst和 evdev.lst 文件内容完全一样,其他俩 xml 文件也一样。为什么一样有什么作用,还不清楚。
以qwerty us布局为例:
在base.lst和 base.xml 中分别查找 English(us),大概可以得出以下结论:
base.lst简单的列出所有的布局以及它们的子节点(也就是 variant),base.xml 文件提供的信息更加丰富。
├── geometry # 1类
├── keycodes # 2类
├── rules # 3类
├── symbols # 2类
如果程序启动的时候XKB根据配置加载指定的 “us” 布局,那么当用户在使用时敲击 ‘`’ 这个键,事件处理过程如下:
键盘发出keycode 49的值,XKB 从 keycodes/evdev 中找到 keycode 49 对应的 symbol 为
Xkb的 level 和 group
Level比较简单,比如 qwerty 中,通过按住修饰键可以得到不同字符,比如直接按 a,通过修饰符 shift 就得到 A,这种直接关联的就是 level 。
Group表示将键盘的字符集整个切换,一般来说就是非英语环境的字符集,我们大汉字就是与英语完全不同的字符集。每个 group 下面也有不同的 level,比如非英文字符集情况下, æ可以切换为Æ。
当收到XKB 键盘事件好后,需要知道这个事件对应的 group 与 shift level 才能确定对应的字符是什么。
修改XKB配置 setxkbmap
如果想修改XKB配置文件,或者想换个口味试试其他的键盘布局,最好不要自己手动修改配置文件,一不小心就弄坏了。最简单的方式就是命令行工具 setxkbmap
setxkbmap -layout us
但是如果想要混合使用qwerty us布局和 dvorak us variant,那么:
setxkbmap -layout us,us -variant ,dvorak
不过这条命令慎用,setxkbmap对语法要求比较严格:layouts 和 variants 数目必须一一对应。上面命令中 dvorak 前面多一个逗号,等同于这个 -layout us,us -variant «null»,dvorak
如果你不知道当前键盘布局对应的variants,可以根据名称去查,symbols/us 查找字段为 xkb_symbols。[rgrep "xkb_symbols" symbols/us]
可以通过设置提示信息级别来更详细地打印输出:
setxkbmap -print -verbose 10
2、X 相关的接口
XChangeProperty
参数:
XConnectionNumber
参数:
Display: 到 X server 的一个连接
返回连接到X server的 display 值, 0,1?等等
3、Mutter中如何监听 Xorg 事件
Mutter中通过创建事件源 GSource,attach 到 glib 主循环的方式,让 glib 的上下文来处理。监听的 fd 为 XDisplay。
Glib中的事件处理
先简单的了解下GMainLoop, GMainContext和GSource。
要让GMainLoop能够处理事件,首先就必须把它们加到GMainLoop去。首先我们需要了解事件循环的三个基本结构:GMainLoop, GMainContext和GSource。
它们之间的关系是这样的:
GMainLoop -> GMainContext -> {GSource1, GSource2, GSource3......}
每个GmainLoop都包含一个GMainContext成员,而这个GMainContext成员可以装各种各样的GSource,GSource则是具体的各种Event处理逻辑了。在这里,可以把GMainContext理解为GSource的容器。(不过它的用处不只是装GSource)
创建GMainLoop使用函数g_main_loop_new, 它的第一个参数就是需要关联的GMainContext,如果这个值为空,程序会分配一个默认的Context给GMainLoop。把GSource加到GMainContext呢,则使用函数g_source_attach。
GLib内部定义实现了三种类型的事件源,分别是 Idle, Timeout 和 I/O。Mutter 中自定义事件源就是 I/O 类型的。自定义事件源的关键是实现下面几个接口。
gboolean (*prepare) (GSource *source, gint *timeout_);
进入睡眠之前,在g_main_context_prepare里,mainloop调用所有Source的prepare函数,计算最小的timeout时间,该时间决定下一次睡眠的时间。
gboolean (*check) (GSource *source);
poll被唤醒后,在 g_main_context_check里,mainloop调用所有Source的check函数,检查是否有Source已经准备好了。如果poll是由于错误或者超时等原因唤醒的,就不必进行dispatch了。
gboolean (*dispatch) (GSource*source, GSourceFunc callback,gpointer user_data);
当有Source准备好了,在 g_main_context_dispatch里,mainloop调用所有Source的dispatch函数,去分发消息。
void (*finalize) (GSource *source);
在Source被移出时,mainloop调用该函数去销毁Source。
Mutter中监听连接到 Xserver 的文件句柄(ConnectionNumber),监听 g_io_in 类型的事件,并在事件分发时判断是不是 xkb 的 XkbStateNotify 信号,如果是,判断这个事件结构中 group 信息是否与 mutter 一致,如果不一致,停留在 mutter 自己的 group。
五、小结
这个问题涉及三个部分:输入法、窗口管理器和XKB扩展。
参考:
科普说明:
https://medium.com/@damko/a-simple-humble-but-comprehensive-guide-to-xkb-for-linux-6f1ad5e13450
XKB扩展说明
https://www.x.org/releases/X11R7.6/doc/libX11/specs/XKB/xkblib.html
Mutter修改对应的缺陷
https://bugzilla.gnome.org/show_bug.cgi?id=756543