ibus输入法开发记录:(三)属性菜单IBusProperty和配置IBusConfig
属性菜单
原生的ibus通过属性菜单提供即时变更输入法配置的方法。如下图所示,左侧的状态条内的图标与右侧红框内的属性菜单为一一对应关系,例如点击状态条里“中”的图标与属性菜单里“CN”的字样,都能够触发相同prop_name
的回调函数,产生同样的效果。
ibus的状态条默认是关闭的,一般可以通过设置唤出。在我的ubuntu16.04平台,可以在终端敲入
$ ibus-setup
然后从打开的设置中将“显示属性栏”设置为“总是”
状态条可以辅助直观地指示一些常用的当前输入状态,例如中英文模式、全半角标点、简繁体模式等,比较符合windows下有状态栏的搜狗、紫光等输入法的使用习惯。不同的平台有不同的状态条开启方法,但我不保证有,如果需要用到,请自行查阅具体平台的设置方法。
初始化属性菜单
属性菜单一般在引擎的init中初始化:
static void ibus_myime_engine_init(IBusMyIMEEngine *engine)
{
// 首先初始化下拉属性菜单列表
engine->props = ibus_prop_list_new();
g_object_ref_sink(engine->props);
// 然后为其逐个添加属性
engine->p_mode = ibus_property_new(...);
ibus_prop_list_append(engine->props, engine->p_mode);
}
然后,调用ibus_engine_register_properties
完成属性菜单的注册:
static void ibus_myime_engine_focus_in(IBusMyIMEEngine *engine)
{
ibus_engine_register_properties((IBusEngine*)engine, engine->props);
}
由于ibus-pinyin的属性菜单是在focus_in
的回调中注册的,所以我也写在了这里。也不知道有什么讲究,大家可以也写在init里测试一下。
ibus_property_new
需要的参数如下:
IBusProperty * ibus_property_new (const gchar *key,
IBusPropType type,
IBusText *label,
const gchar *icon,
IBusText *tooltip,
gboolean sensitive,
gboolean visible,
IBusPropState state,
IBusPropList *prop_list);
其中,key
是标识该属性的唯一字符串,同时也是属性变更时传给回调函数的prop_name
。用户可以根据这一字符串区分不同的属性。
属性变化:回调与显示更新
当状态条上的按钮或对应的下拉菜单中的属性被点击时,会触发一个property_activate
的回调函数,其注册与定义如下:
// 注册
engine_class->property_activate = ibus_myime_property_activate;
// 定义
static void ibus_myime_property_activate(IBusMyIMEEngine *engine,
const gchar *prop_name,
gint prop_state)
{
if (strcmp(prop_name, "your_prop_name") == 0) {
// your code here...
}
}
由于属性的回调中可以正确获取到当前engine
的地址,因此可以通过对属性的点击,在回调函数中操作engine
内当前输入法的状态变量,即时变更输入法状态的设置。这应该是ibus比较推荐的方法。还有通过设置界面来完成更复杂且具体设置项的方法,我将在下面的IBusConfig部分讲到。
动态地变属性显示的文字、图标、tip等内容可以采用以下方法:
IBusText *label, *tip;
gchar *icon;
// 定义图标或文字内容
// ...
// 设置属性文字(下拉菜单显示)
ibus_property_set_label(engine->p_mode, label);
// 设置属性tip(鼠标悬浮时显示)
ibus_property_set_tooltip(engine->p_mode, tip);
// 设置属性图标(状态条显示)
ibus_property_set_icon(engine->p_mode, icon);
// 设置完成后,通知引擎更新属性显示内容
ibus_engine_update_property((IBusEngine*)engine, engine->p_mode);
配置项IBusConfig
ibus本身在一定程度上支持通过启动自定义的设置界面完成输入法配置的功能,例如点开ibus-pinyin下拉菜单中的“首选项”:
在ibus的设计中,这个界面是一个由python gtk实现的、与ibus和ibus-pinyin完全独立的应用程序。“首选项”作为下拉菜单中的一个属性被定义,用户点击后触发ibus_myime_property_activate
回调,然后可以在其中启动相应的设置。可以将该python脚本的绝对路径定义在xml文件的<setup>
标签中,然后从engine
的<setup>
读取,当然也可以在代码中写死固定路径:
static void ibus_myime_property_activate(IBusMyIMEEngine *engine,
const gchar *prop_name,
gint prop_state)
{
if (strcmp(prop_name, "p_setup") == 0) {
IBusEngineDesc *engine_desc = ibus_bus_get_global_engine(bus);
gchar setup[1024];
const gchar *setup_path = ibus_engine_desc_get_setup(engine_desc);
g_spawn_command_line_async (setup, NULL);
// 这里要对engine_desc解引用,不然会发生内存泄漏
// 重新翻看了一下ibus的文档说don't free data after the code is done
// 但用valgrind做memcheck的时候不写这行确实会有与设置次数点击正相关的definitely lost
g_object_unref (G_OBJECT (engine_desc));
}
}
python gtk编写的首选项
gtk确实是对意志的磨练和对生命的摧残,如果有条件,可以试试别的语言和界面库
不知道不调用另外的程序,直接在engine的程序里启动新的界面有什么影响,欢迎探讨
首选项作为一个单独的应用程序,通过IBusConfig模块与engine发生数据交换。在engine中,config的内容通过以下代码写入(以一个整数为例):
ibus_config_set_value(config, section, name, g_variant_new_int32(value));
ibus的config通过名称空间section
管理,一个section
下可以有多个name-value
对,不同section
下的name
可以重复。一般一个输入法引擎拥有一个独立的名称空间,例如将myime的中英输入模式写入设置:
#define MODE_ZH 0
#define MODE_EN 1
ibus_config_set_value(config, "engine/myime", "mode", g_variant_new_int32(MODE_ZH));
python通过以下代码连接ibus和获取指定名称空间下的name-value
对:
self.__bus = IBus.Bus()
self._config = self.__bus.get_config()
self._config_values = dict(self._config.get_values(self._config_section))
python同样可以向指定名称空间写入设置:
self._config.set_value(self._config_section, name, GLib.Variant.new_int32(value))
当config
中的值被设置或者更改时,ibus会发出一个信号,因此在engine
的代码中,我们可以注册一个自定义函数监听这个信号:
g_signal_connect(config, "value-changed", ibus_myime_config_changed, NULL);
void ibus_myime_config_changed(IBusConfig *config,
gchar *section,
gchar *name,
GVariant *value,
gpointer *user_data)
{
// your code here...
}
用这种方法,可以使engine
所在的程序获知首选项中的设置内容发生了改变,从而做出相应的动作。
IBusConfig产生的一些问题
使用IBusConfig有以下几个问题:
第一,连接到value-changed
信号后,其他对config内容发生修改的信号同样会被我们的engine捕捉到,包括其他ibus输入法在设置时产生的信号、以及ibus切换时本身发出的信号。因此,必须在ibus_myime_config_changed
函数的最前面对section
(名称空间)进行过滤,只处理本名称空间下的config变化;或者利用ibus_config_watch
只监听需要的配置名字。
第二,ibus的config模块是由ibus实现和保存的,当调用了ibus_config_set_value
后,除非调用了ibus_config_unset
,这个值将永远被保存在ibus中。因此,当config模块被用作输入法的配置方式时,根据这个值的用途,必须非常注意在首选项打开前或配置项读取前对config内部的值的初始化。例如:假设某输入法的中英模式属性值在初次启动时必然为0,但在使用后config里被设置为1;如果下次重启后直接从config读取而未经初始化,读取的值就是上次设置的1。
第三也是最重要的一点,ibus_myime_config_changed
不像其他engine_class
的回调函数,它的参数里没有当前engine
的地址,它通常情况下也无法获取到当前的engine
。而且,由于engine
销毁不代表我们的输入法程序结束,因此在别的输入法使用时,ibus_myime_config_changed
还能继续被value-changed
信号触发。我认为一些场合也需要考虑到别的ibus输入法进程在当前输入法没有使用时恶意写入配置并调用到ibus_myime_config_changed
的情况。
那么这种机制下要怎么让ibus_myime_config_changed
正确地通知到engine
并即时地发生状态修改呢??答案是没办法!!!观察了Ibus-pinyin的首选项,必须重启ibus(用户注销重新登录或系统重启,或单独重启ibus进程)才能使配置生效!!!!!
因此,我采用的方式是在engine的init
函数中导出当前engine的全局变量,在destroy
函数中将他置为NULL
,供ibus_myime_config_changed
随时调用。结合ibus输入法开发记录:(二)引擎engine提到的关于引擎init与destroy的问题,为了防止init与destroy错位的情况发生,在destroy
中置空操作修改如下:
IBusMyIMEEngine *myime_engine;
static void ibus_myimem_engine_init(IBusMyIMEEngine *engine)
{
// other code
// ...
myime_engine = engine;
}
static void ibus_myime_engine_destroy (IBusMyIMEEngine *engine)
{
// other code
// ...
// qxy: 在托盘菜单中已选中当前ime的情况下再点击当前ime,
// 会先init(新的ime)在destroy(旧的ime)
// 两个方法传参的engine地址不一样,可以理解为init的engine是新new的
// 若直接把全局变量的myime_engine赋值null,一些情况下会发生程序认为输入法启动后马上被销毁,设置不起效的情况
// 但为了及时保存设置
// 必须导出当前的engine给config的回调函数
// 目前的策略是init时赋值myime_engine=engine
// destroy时判断myime_engine是否与engine为同一个,若是,再销毁和赋空
if (myime_engine == engine) {
engine->ime = NULL;
myime_engine = NULL;
}
((IBusObjectClass *) ibus_myime_engine_parent_class)->destroy ((IBusObject *)engine);
}
同样,如果有引擎内有其他生命周期与输入法进程一致的资源,必须以相同的思路斟酌是否应该在destroy
中销毁以及如何销毁。
相关文章
ibus输入法开发记录:(一)概览
ibus输入法开发记录:(二)引擎engine
ibus输入法开发记录:(三)属性菜单IBusProperty和配置IBusConfig ←你在这里