ibus的引擎(engine)是提供输入功能的核心。对于用户而言,一个engine就是一个可选择使用的输入法,如下图所示:
列表中安装的输入法实际上有英语、SunPinyin、Pinyin(和Bopomofo)三个组件(component),但总共有四个输入引擎。Pinyin和Bopomofo实际被包含在一个组件之中,即属于同一个可执行程序。
将多个引擎打包到一个组件中可以根据需求增强引擎间一些共享资源的关联,但引擎engine才能够被用户认知为一个独立的输入法
除了多引擎之外,另一种多输入法的实现方法可以参考ibus-rime的实现,它在一个组件可执行程序内仅实现一个输入法引擎engine,但可以通过键盘选择切换实现方案,这些方案作为不同的动态库,导出通用功能接口被engine所使用,方案内部处理具体的按键事件并返回处理结果,使一个输入引擎(engine)内能够通过自定义按键操作动态地切换拼音、五笔等输入方案
GTK中实现了GObject对象系统,它使C语言的语法能够模拟C++的面向对象行为,使C语言中可以也做到例如类的继承、构造等操作,十分复杂和巧妙。构建ibus的自定义引擎,需要实现两个继承于“基类”的结构体(类):
typedef struct _IBusMyIMEEngine IBusMyIMEEngine;
typedef struct _IBusMyIMEEngineClass IBusMyIMEEngineClass;
struct _IBusMyIMEEngine {
IBusEngine parent;
/* members */
IBusLookupTable *table;
IBusPropList *props;
MY_IME *ime;
};
struct _IBusMyIMEEngineClass {
IBusEngineClass parent;
};
IBusMyIMEEngineClass
继承自IBusEngineClass
,其内部实现ibus需要引擎自定义实现的输入接口,包括键盘事件、焦点变化、上下翻页、候选词点击选择、属性(状态栏)点击事件以及引擎销毁destroy等在该引擎中的具体行为。当相应的事件产生时,ibus将调用当前引擎的相应方法并将事件内容作为参数传递到方法内部,由我们的自定义实现对事件进行处理并将结果返回。IBusEngineClass
中的常用接口包括如下:
接口 | 描述 |
---|---|
destroy | 引擎实例销毁 |
process_key_event | 键盘事件处理 |
focus_in | IBus(包括托盘图标)获取到焦点 |
focus_out | IBus失去焦点 |
page_up | 输入面板的“向上翻页”功能被选择 |
page_down | 输入面板的“向下翻页”功能被选择 |
property_activate | 属性(包括托盘下拉菜单和状态栏按钮)被选择 |
set_cursor_location | 光标位置改变 |
candidate_clicked | 输入面板的候选词被点击 |
其他接口及参数详见ibus手册:https://ibus.github.io/docs/ibus-1.5/IBusEngine.html#IBusEngineClass
IBusMyIMEEngine
继承自IBusEngine
,结构体内除了第一个成员必须为父亲IBusEngine
,其余成员均可作为当前引擎的成员变量自由定义,如当前引擎的属性、候选查询表、使用的内核及当前状态等等。在事件产生、引擎接口被调用时,当前IBusMyIMEEngine
的地址将作为参数被传回,我们可以自由访问其内部的成员变量。
GTK中通过宏定义G_DEFINE_TYPE
实现GObject对象与其初始化函数class_init
和init
的绑定,实现类似于对象与构造函数的效果,这两个函数中分别实现对IBusMyIMEEngineClass
和IBusMyIMEEngine
内容的初始化。
G_DEFINE_TYPE
的调用如下:
G_DEFINE_TYPE(MyObject, my_object, G_TYPE_OBJECT);
该宏定义为自定义GObject对象自动生成注册到对象系统所必要的初始化函数,展开后代码大致所示:
static void my_object_init(MyObject * self);
static void my_object_class_init(MyObjectClass * klass);
static gpointer my_object_parent_class = ((void *) 0);
static gint MyObject_private_offset;
static void
my_object_class_intern_init(gpointer klass)
{
my_object_parent_class = g_type_class_peek_parent(klass);
if (MyObject_private_offset != 0)
g_type_class_adjust_private_offset(klass, &MyObject_private_offset);
my_object_class_init((MyObjectClass *) klass);
}
__attribute__ ((__unused__))
static inline gpointer
my_object_get_instance_private(const MyObject * self)
{
return (((gpointer) ((guint8 *) (self) + (glong) (MyObject_private_offset))));
}
GType
my_object_get_type(void)
{
static volatile gsize g_define_type_id__volatile = 0;
if (g_once_init_enter(&g_define_type_id__volatile)) {
GType g_define_type_id = g_type_register_static_simple(
((GType) ((20) << (2))),
g_intern_static_string("MyObject"),
sizeof(MyObjectClass),
(GClassInitFunc) my_object_class_intern_init,
sizeof(MyObject),
(GInstanceInitFunc) my_object_init,
(GTypeFlags) 0);
}
return g_define_type_id__volatile;
};
get_type
函数中的内容较为复杂,因此这里描述时做了适当简化,它实为GObject对象实例化的接入口,提供了注册和管理用户自定义引擎对象类型的技术实现,使GObject对象系统能够调用到自定义对象的class_init
和init
函数。一般GTK程序还将导出get_type
函数的定义,作为“构造函数”供其他模块调用,从而完成类的初始化:
#define IBUS_TYPE_MYIME_ENGINE \
(ibus_myime_engine_get_type())
GType ibus_myime_engine_get_type(void);
在G_DEFINE_TYPE
的宏定义展开中定义了my_object_init
和my_object_class_init
两个函数,对我们的输入法引擎实现,其被展开如下:
G_DEFINE_TYPE(IBusMyIMEEngine, ibus_myime_engine, IBUS_TYPE_ENGINE);
↓
static void ibus_myime_engine_init(IBusMyIMEEngine* self);
static void ibus_myime_engine_class_init(IBusMyIMEEngineClass * klass);
因此在代码中,我们需要实现这两个函数。
ibus_myime_engine_class_init
用于初始化IBusMyIMEEngineClass
,它在引擎类向ibus注册时被调用,在我们的输入法组件生命周期中仅调用一次。一般ibus_myime_engine_class_init
的内部实现如下:
static void ibus_myime_engine_class_init(IBusMyIMEEngineClass *klass)
{
INFO("ibus_myime_engine_class_init");
IBusObjectClass *ibus_object_class = IBUS_OBJECT_CLASS (klass);
IBusEngineClass *engine_class = IBUS_ENGINE_CLASS (klass);
// 初始化引擎销毁接口
ibus_object_class->destroy = (IBusObjectDestroyFunc) ibus_myime_engine_destroy;
// 初始化引擎其他必要接口
engine_class->process_key_event = ibus_myime_engine_process_key_event;
engine_class->focus_in = ibus_myime_engine_focus_in;
engine_class->focus_out = ibus_myime_engine_focus_out;
...
}
ibus_myime_engine_init
用于初始化与我们实现的自定义输入法有关的数据结构,如IBusMyIMEEngine
内部成员,或输入发使用的字词表。详见下一节的描述。
ibus_myime_engine_init
和ibus_object_class->destroy
分别用于初始化和销毁IBusMyIMEEngine
,初始化函数在用户切换进入当前输入法引擎时调用,销毁函数则在用户切换离开当前输入法引擎时调用,与初始化函数的调用一一对应。
值得注意的是,具有对应关系的init
和destroy
函数参数的IBusMyIMEEngine
地址是相同的,且destroy
始终发生在init
之后;然而,非对应关系的init
和destroy
之间的顺序关系是不可预知的。通过以下两种情形举例说明:
A_init
,离开A时会调用A_destroy
;进入B时会调用B_init
,离开B时会调用B_destroy
。A_init
必定在A_destroy
后且两者参数的engine
地址一致;然而B_init
则有可能发生在A_destroy
前。A_destroy
)和再次创建A(调用A_init
),但是新的A_init
可能发生在A_destroy
之前,如下图所示。此时两次调用中参数engine
的地址不一致,A_destroy
匹配的是前一次A_init
。这对正常的ibus切换逻辑没有影响,但倘若我们直接简单地认为init
就是创建destroy
就是销毁,并在里面直接初始化和销毁一些非engine
内部的公共资源,就可能会引起输入法的崩溃。以上现象是从ubuntu16.04,ibus1.5.11观察到的。除了情形2中的切换方法,在系统登录、ibus第一次启动时,引擎也可能发生情形2这样的行为。在我的场景中,为了导出IBusMyIMEEngine
供外部使用,在init
中为全局变量赋值并在destroy
中置空,这一做法使输入法变得极不稳定。后续使用的实现方法将在后续博客中说明,若能有更好的、更稳定的实现方法,欢迎共同探讨。
当用户切换当前使用的ibus输入法时,ibus将会同步切换当前的engine_class,从而使事件能够正确传递到当前的引擎实现中。以process_key_event
为例,ibus_myime_engine_process_key_event
的实现可以如下:
static gboolean
ibus_myime_engine_process_key_event(IBusMyIMEEngine *engine,
guint keyval,
guint keycode,
guint modifiers)
{
INFO("keyval=%d(%x), keycode=%d(%x), modifiers=%d(%x)\n", keyval, keyval, keycode, keycode, modifiers, modifiers);
gboolean ret = engine->ime->process(keyval);
return ret;
}
此外,ibus也提供ibus_engine_commit_text
、ibus_engine_update_lookup_table
等方法用于结果上屏或输入面板更新,详情可查询ibus手册,此处不再赘述
ibus引擎在组件初始化时通过ibus_factory_add_engine(factory, "myime", IBUS_TYPE_MYIME_ENGINE);
向ibus完成添加,它将输入法引擎实现与定义于xml文件中的engine name相关联。第三个参数即为上文介绍的ibus_myime_engine_get_type
,用于ibus获取初始化引擎的方法。这一方法也是ibus在初始化和用户切换输入法时调取ibus_myime_engine_class_init
和ibus_myime_engine_init
的重要入口。
ibus输入法开发记录:(一)概览
ibus输入法开发记录:(二)引擎engine ←你在这里
ibus输入法开发记录:(三)属性菜单IBusProperty和配置IBusConfig