Difference between revisions of "Develop an simple input method/zh-cn"
Matrikslee (talk | contribs) (Created page with "== 存储不同输入上下文的状态 == Fcitx 允许不同的输入上下文保持不同的状态。状态通常是指部分类型化的文本和所有其他关联的数据...") |
Matrikslee (talk | contribs) (Created page with "工厂类带有一个方便的 C++ 模板,[https://codedocs.xyz/fcitx/fcitx5/group__FcitxCore.html#ga9e60042d1f671a6fa31ea04bb4961ec9 FactoryFor]。这实际上是 [https://...") |
||
Line 267: | Line 267: | ||
Fcitx 允许不同的输入上下文保持不同的状态。状态通常是指部分类型化的文本和所有其他关联的数据结构。在这个区位码的例子里面,状态是用户已经输入的数字。为了表示这一点,Fcitx 提供了一个方便的类 [https://codedocs.xyz/fcitx/fcitx5/classfcitx_1_1InputBuffer.html InputBuffer] 让输入法引擎可以便捷地使用这个类来编辑内部状态。为了在构造输入上下文时自动构造状态,Fcitx 提供了一个名为 InputContextProperty 的框架。为了使用它,您首先需要通过 registerProperty 向 [https://codedocs.xyz/fcitx/fcitx5/classfcitx_1_1InputContextManager.html InputContextManager] 注册一个工厂类。每个属性都需要有一个全局唯一的名称。 名称可以是人类可以理解的东西。在这个区位码的例子里面,我使用的是“quweiState”。使用更好的名称的好处是,以防万一您正在开发一些交叉插件(另一个插件需要访问该插件的某些内部状态),您可以使用这个通用名称将其加载到不同的插件中。如果您不需要从外部访问,那么名称并不重要。 | Fcitx 允许不同的输入上下文保持不同的状态。状态通常是指部分类型化的文本和所有其他关联的数据结构。在这个区位码的例子里面,状态是用户已经输入的数字。为了表示这一点,Fcitx 提供了一个方便的类 [https://codedocs.xyz/fcitx/fcitx5/classfcitx_1_1InputBuffer.html InputBuffer] 让输入法引擎可以便捷地使用这个类来编辑内部状态。为了在构造输入上下文时自动构造状态,Fcitx 提供了一个名为 InputContextProperty 的框架。为了使用它,您首先需要通过 registerProperty 向 [https://codedocs.xyz/fcitx/fcitx5/classfcitx_1_1InputContextManager.html InputContextManager] 注册一个工厂类。每个属性都需要有一个全局唯一的名称。 名称可以是人类可以理解的东西。在这个区位码的例子里面,我使用的是“quweiState”。使用更好的名称的好处是,以防万一您正在开发一些交叉插件(另一个插件需要访问该插件的某些内部状态),您可以使用这个通用名称将其加载到不同的插件中。如果您不需要从外部访问,那么名称并不重要。 | ||
− | + | 工厂类带有一个方便的 C++ 模板,[https://codedocs.xyz/fcitx/fcitx5/group__FcitxCore.html#ga9e60042d1f671a6fa31ea04bb4961ec9 FactoryFor]。这实际上是 [https://codedocs.xyz/fcitx/fcitx5/classfcitx_1_1LambdaInputContextPropertyFactory.html LambdaInputContextPropertyFactory] 的类型别名。该类仅接受 lambda 函数作为工厂实现。这可以节省您创建自己的 [https://codedocs.xyz/fcitx/fcitx5/classfcitx_1_1InputContextPropertyFactory.html InputContextPropertyFactory] 子类的时间。 | |
In order to get the state object from input context, you can simply use the factory object like this: | In order to get the state object from input context, you can simply use the factory object like this: |
Revision as of 03:38, 19 April 2023
这是编写 Fcitx 5 输入法的分步说明。同样的指令可以用于开发其他类型的插件,只是输入法引擎是最复杂的。
了解 Fcitx 共享库插件的文件结构
Fcitx 5 提供了一个用于添加新插件类型的可扩展框架,但共享库支持是内置的,并且是所有其他插件类型的基础。所以我们只会在本文档中介绍共享库插件。
[fcitx install prefix] | |- share/fcitx5 | | | |- addons/[addon name].conf | |- inputmethod/[input method name 1].conf | | ... | |- inputmethod/[input method name n].conf | |- lib/fcitx5 | |- [library name].so
以上是输入法插件的文件结构。对于其他类型的插件,不需要 inputmethod/
下的文件。[addon name].conf
的文件名很重要,将用于唯一引用此特定插件。Fcitx 也遵循 XDG 目录标准,所以 XDG_DATA_DIR/fcitx5 下的文件也会被检查。同样,inputmethod/
下的配置文件的文件名也很重要,将是某个输入法的唯一名称。
[addon name].conf
示例
[Addon] Name[ca]=Pinyin Name[da]=Pinyin Name[de]=Pinyin Name[he]=פיניין: Name[ko]=병음 Name[ru]=Пиньинь Name[zh_CN]=拼音 Name=Pinyin Category=InputMethod Version=5.0.8 Library=pinyin Type=SharedLibrary OnDemand=True Configurable=True [Addon/Dependencies] 0=punctuation [Addon/OptionalDependencies] 0=fullwidth 1=quickphrase 2=cloudpinyin 3=notifications 4=spell 5=pinyinhelper 6=chttrans 7=imeapi
[输入法名称].conf
示例
[InputMethod] Name[ca]=Pinyin Name[da]=Pinyin Name[de]=Pinyin Name[he]=פיניין: Name[ko]=병음 Name[ru]=Пиньинь Name[zh_CN]=拼音 Name=Pinyin Icon=fcitx-pinyin Label=拼 LangCode=zh_CN Addon=pinyin Configurable=True
该文件采用类似 ini 的格式,具有某些 fcitx 特定的扩展名和规则。它还支持 XDG 桌面文件样式 i18n 进行翻译。
使用 CMake 构建系统
理论上只要能生成正确的文件,就可以自由选择要使用的构建系统。但是Fcitx 5 对 CMake 提供了大量支持,因此使用 CMake 将是构建 Fcitx 项目的最便捷方式。在本文档中,我们将仅介绍使用 CMake 作为构建系统。
快速入门:区位码
区位码 输入法是一种输入法,基本上允许您输入GB2312的数字并产生与该代码匹配的汉字。它曾经被 Fcitx 4 支持,但不再被 Fcitx 5 包含。虽然它很难使用,但它可以作为一个很好的例子来说明如何为 Fcitx 5 实现一个简单的输入法。
项目框架
因此,让我们从这个项目的框架开始。
├── CMakeLists.txt ├── LICENSES │ └── BSD-3-Clause.txt # License for this project ├── po # Optional I18n │ ├── CMakeLists.txt │ ├── fcitx5-quwei.pot │ ├── LINGUAS │ └── zh_CN.po └── src ├── CMakeLists.txt ├── quwei-addon.conf.in.in # Addon registration file ├── quwei.conf.in # Input method registration file ├── quwei.cpp # Engine implementation └── quwei.h # Engine implementation
您也许需要查看一些 CMake 教程以了解一些基本的 CMake 用法。
CMakeLists.txt 在根目录查找依赖项。
cmake_minimum_required(VERSION 3.21) project(fcitx5-quwei) find_package(Fcitx5Core REQUIRED) # Setup some compiler option that is generally useful and compatible with Fcitx 5 (C++17) include("${FCITX_INSTALL_CMAKECONFIG_DIR}/Fcitx5Utils/Fcitx5CompilerSettings.cmake") add_subdirectory(src)
src/CMakeLists.txt 示例如下:
# Make sure it produce quwei.so instead of libquwei.so add_library(quwei SHARED quwei.cpp) target_link_libraries(quwei PRIVATE Fcitx5::Core) set_target_properties(quwei PROPERTIES PREFIX "") install(TARGETS quwei DESTINATION "${FCITX_INSTALL_LIBDIR}/fcitx5") # Addon config file # We need additional layer of conversion because we want PROJECT_VERSION in it. configure_file(quwei-addon.conf.in quwei-addon.conf) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/quwei-addon.conf" RENAME quwei.conf DESTINATION "${FCITX_INSTALL_PKGDATADIR}/addon") # Input Method registration file install(FILES "quwei.conf" DESTINATION "${FCITX_INSTALL_PKGDATADIR}/inputmethod")
quwei.conf.in 示例:
[InputMethod] # Translatable name of the input method Name=Quwei # Icon name Icon=fcitx-quwei # A short label that present the name of input method Label=区 # ISO 639 language code LangCode=zh_CN # Match addon name Addon=quwei # Whether this input method support customization # Configurable=True
此文件将作为 InputMethodEntry 对象进行加载。
quwei-addon.conf.in 示例:
[Addon] Name=Quwei Category=InputMethod Version=@PROJECT_VERSION@ Library=quwei Type=SharedLibrary OnDemand=True Configurable=True [Addon/Dependencies] 0=punctuation [Addon/OptionalDependencies] 0=fullwidth 1=quickphrase 2=chttrans
此文件将作为 AddonInfo 对象加载。
InputMethodEngine 的基本实现
您可以参考github中第一笔提交的代码。
版本 1 的 quwei.h
/* * SPDX-FileCopyrightText: 2021~2021 CSSlayer <wengxt@gmail.com> * * SPDX-License-Identifier: BSD-3-Clause * */ #ifndef _FCITX5_QUWEI_QUWEI_H_ #define _FCITX5_QUWEI_QUWEI_H_ #include <fcitx/inputmethodengine.h> #include <fcitx/addonfactory.h> class QuweiEngine : public fcitx::InputMethodEngineV2 { void keyEvent(const fcitx::InputMethodEntry & entry, fcitx::KeyEvent & keyEvent) override; }; class QuweiEngineFactory : public fcitx::AddonFactory { fcitx::AddonInstance * create(fcitx::AddonManager * manager) override { FCITX_UNUSED(manager); return new QuweiEngine; } }; #endif // _FCITX5_QUWEI_QUWEI_H_
版本 1 的 quwei.cpp
/* * SPDX-FileCopyrightText: 2021~2021 CSSlayer <wengxt@gmail.com> * * SPDX-License-Identifier: BSD-3-Clause * */ #include "quwei.h" void QuweiEngine::keyEvent(const fcitx::InputMethodEntry& entry, fcitx::KeyEvent& keyEvent) { FCITX_UNUSED(entry); FCITX_INFO() << keyEvent.key() << " isRelease=" << keyEvent.isRelease(); } FCITX_ADDON_FACTORY(QuweiEngineFactory);
当实现 Fcitx 插件时,它应该是 AddonInstance 的子类。AddonInstance 的实例化是通过 AddonFactory 完成的。 InputMethodEngineV2 是 AddonInstance 的子类。实现输入法插件时需要使用该类。
一个输入法引擎的最小植入只需要包含keyEvent函数的实现。
在这里,我们使用类似于宏 FCITX_INFO()
的 iostream 将我们按下的每个键写入日志。
这里假设您的 fcitx 安装前缀是 /usr。构建这个项目的命令是:
mkdir -p build cd build cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug # use Debug for easy debugging with gdb make # or ninja, depending on your system sudo make install # or sudo ninja install
如果一切正常,安装命令应该打印出如下内容:
-- Install configuration: "Debug" -- Installing: /usr/lib/fcitx5/quwei.so -- Installing: /usr/share/fcitx5/addon/quwei.conf -- Installing: /usr/share/fcitx5/inputmethod/quwei.conf
现在您可以使用 fcitx5 -rd
重新启动 fcitx5,并使用 配置工具(Fcitx 5) 将 Quwei 添加到您的配置中。
切换到区位码输入法后,应用程序中的按键将使 Fcitx 5 打印出如下内容:
I2021-11-16 12:29:32.352702 quwei.cpp:12] Key(f states=0) isRelease=1 I2021-11-16 12:29:32.389935 quwei.cpp:12] Key(s states=0) isRelease=0 I2021-11-16 12:29:32.413689 quwei.cpp:12] Key(d states=0) isRelease=0 I2021-11-16 12:29:32.497661 quwei.cpp:12] Key(s states=0) isRelease=1 I2021-11-16 12:29:32.498021 quwei.cpp:12] Key(f states=0) isRelease=0 I2021-11-16 12:29:32.523816 quwei.cpp:12] Key(a states=0) isRelease=1 I2021-11-16 12:29:32.524051 quwei.cpp:12] Key(d states=0) isRelease=1 I2021-11-16 12:29:32.704919 quwei.cpp:12] Key(f states=0) isRelease=1 I2021-11-16 12:29:32.705006 quwei.cpp:12] Key(d states=0) isRelease=0 I2021-11-16 12:29:32.833024 quwei.cpp:12] Key(d states=0) isRelease=1 I2021-11-16 12:29:34.633936 quwei.cpp:12] Key(Control_L states=0) isRelease=0 I2021-11-16 12:29:35.053817 quwei.cpp:12] Key(Control+C states=4) isRelease=0 I2021-11-16 12:29:35.165617 quwei.cpp:12] Key(Control+C states=4) isRelease=1 I2021-11-16 12:29:35.348654 quwei.cpp:12] Key(Control+Control_L states=4) isRelease=1
从这里可以知道您的输入法引擎现在可以正常工作了。
实现输入法逻辑
区位码的基本逻辑是键入 4 位数的区位代码。区位码可以看成区码 xx 和位码 yy。区位码到GB2312的映射是(0xA0 + 区码, 0xA0 + 位码)。当用户键入 3 个数字的区位码时,输入法将显示一个包含 10 个可能带有给定区码为前缀的字符的候选列表。
可以参考 github 中 第二笔提交 的代码。
存储不同输入上下文的状态
Fcitx 允许不同的输入上下文保持不同的状态。状态通常是指部分类型化的文本和所有其他关联的数据结构。在这个区位码的例子里面,状态是用户已经输入的数字。为了表示这一点,Fcitx 提供了一个方便的类 InputBuffer 让输入法引擎可以便捷地使用这个类来编辑内部状态。为了在构造输入上下文时自动构造状态,Fcitx 提供了一个名为 InputContextProperty 的框架。为了使用它,您首先需要通过 registerProperty 向 InputContextManager 注册一个工厂类。每个属性都需要有一个全局唯一的名称。 名称可以是人类可以理解的东西。在这个区位码的例子里面,我使用的是“quweiState”。使用更好的名称的好处是,以防万一您正在开发一些交叉插件(另一个插件需要访问该插件的某些内部状态),您可以使用这个通用名称将其加载到不同的插件中。如果您不需要从外部访问,那么名称并不重要。
工厂类带有一个方便的 C++ 模板,FactoryFor。这实际上是 LambdaInputContextPropertyFactory 的类型别名。该类仅接受 lambda 函数作为工厂实现。这可以节省您创建自己的 InputContextPropertyFactory 子类的时间。
In order to get the state object from input context, you can simply use the factory object like this:
auto *state = ic->propertyFor(&factory_);
In certain cases, it is also possible to un-register the factory and re-register it again in order to "refresh" all the internal state.
Candidate List
In Fcitx 5, a candidate list is a part of the InputPanel class, stored as shared_ptr to avoid life time issue when selecting candidate triggers the User interface update. There's different capability provided by candidate list via certain interfaces. The helper class CommonCandidateList would provide most common functionality for a candidate list. It implements BulkCandidateList interface, and that is why it is not suitable for Quwei case. Because we want to have a semi-infinite candidate list for Quwei.
In Quwei, QuweiCandidateList gonna implement a PageableCandidate interface, which allows a prev/next page button to be displayed in the input method panel.
Preedit
Preedit can refer to two different user interface element, one is the preedit embedded within the application, usually referred as "client preedit" in Fcitx. Another one is displayed with in the input method panel window. Usually, input method engine would only use one of them because there is only 1 form of preedit that need to be displayed. Whether the input context supports it or not can be checked via the capabilityFlags property.
Also there's something you may want to consider when using client preedit. Due to the different implementation in toolkit, toolkit may choose to commit the client preedit right away when application loses focus. For certain input methods, it might be designed to work in a way that typing does not need extra confirmation, for example, the word completion mode in keyboard engine. In such case, even though the preedit is used, user would expect the text it to be committed even if it is still in a preedit mode. In that sense, the preedit text should be exact same of the text that will be committed after confirmation.
In some version of iOS, its Pinyin input method using its client preedit in a way that may causes confusion: user types "nihao", and client preedit is displayed as segmented pinyin "ni hao". When text box loses focus, "ni hao" (extra space) would be committed into the application, and such outcome would never happen in regular usage of Pinyin.
Another thing to consider is where to put the cursor. While it might be natural to display the cursor at the its actual place within preedit, the input panel window will also be displayed at the client preedit cursor position. That means along with user regular typing, when cursor moves, the candidate window would also moves. In some cases, this is not desirable because it causes candidate window to be moved frequently when user types. An alternative way to handle this is to set the client cursor always to be 0. If you need to support display the position of actual cursor, you may use highlight style instead. For example, when you have "ABCD" in preedit and the cursor is between B and C. You may set client cursor to 0, and make "AB" displayed with highlight style to indicate the position of cursor.
Key event handling
The most common use case is to call filterAndAccept() for all the key event that you intends to handle. Also, for most input method, key release is not relevant and should be pass-through to application. To make input method engine to work with any user input, be sure to consider all the possible key event that may be typed by user. For example, a common error is forgetting to block certain irrelevant key in composing mode and leaking the key into the application. Also, be careful for key event with modifier, you may want to pass through such key to make application specific hotkey still accessible even when user is composing.
Also, a key event has 3 different form of the Key objects. Normally a input method engine may only want to consider only the key() property. origKey() and rawKey() properties are less used. Key property is in a normalized form of key event. That removes "Shift" modifier for certain cases, which makes it easier to handle by the input method engine. For example, upper case A produced by Shift+A and Capslocked A will be the same after normalization. You may refer to the implementation to learn about how this normalization works.
Reuse features from other addons
You may refer to the code at the third commit in the github.
Fcitx provides a mechanism to invoking functions from other addons without having the need of directly linking to them. And also some easy to use CMake macro to look up the addon dependencies.
find_package(Fcitx5Module REQUIRED COMPONENTS Punctuation QuickPhrase)
We add the find_package line above to look up dependencies for Punctuation and QuickPhrase module.
If you want to implement such a module, you can use the following CMake macro
fcitx5_export_module(QuickPhrase TARGET quickphrase BUILD_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}" HEADERS quickphrase_public.h INSTALL)
And it will automatically generate the required CMake config for your addon.
When using other addons in the code, there is a handy macro FCITX_ADDON_DEPENDENCY_LOADER
that will handle the addon loading at the runtime. When it is called for the first time, the dependent addon will be loaded automatically. Here we defines four different dependency in the code:
FCITX_ADDON_DEPENDENCY_LOADER(quickphrase, instance_->addonManager()); FCITX_ADDON_DEPENDENCY_LOADER(punctuation, instance_->addonManager()); private: FCITX_ADDON_DEPENDENCY_LOADER(chttrans, instance_->addonManager()); FCITX_ADDON_DEPENDENCY_LOADER(fullwidth, instance_->addonManager());
The first parameter should be same as the addon name, second parameter is the expression to get the AddonManager object.
This mechanism make it easy to reuse the functionality implemented by other addons and shared the code. For example, classicui queries the X11/wayland connection from xcb addon and wayland addon.
Configuration
While Quwei does not really need this feature, since it is a really common use case so it will be covered in this section. The addon has a few different interface relevant to configuration, such as getConfig, setConfig, getConfigForInputMethod, setConfigForInputMethod, reloadConfig. The getter function would need to return a Configuration object, while setConfig accepts a RawConfig object. reloadConfig will be called to reload the configuration from disk, you may want to call reloadConfig() in the constructor of the addon too.
If Configurable field is set to True in the addon registration file, such method would be called to retrieve the information and Configtool would generate UI for it.
FCITX_CONFIGURATION( ClipboardConfig, KeyListOption triggerKey{this, "TriggerKey", _("Trigger Key"), {Key("Control+semicolon")}, KeyListConstrain()}; KeyListOption pastePrimaryKey{ this, "PastePrimaryKey", _("Paste Primary"), {}, KeyListConstrain()}; Option<int, IntConstrain> numOfEntries{this, "Number of entries", _("Number of entries"), 5, IntConstrain(3, 30)};);
Usually, you will define a sub class of Configuration with the FCITX_CONFIGURATION macro. First argument is the name of the class, and then you just simply add Option as member. Following is a common implemenation of setConfig/getConfig/reloadConfig.
static constexpr char configFile[] = "conf/clipboard.conf"; void reloadConfig() override { readAsIni(config_, configFile); } const Configuration *getConfig() const override { return &config_; } void setConfig(const RawConfig &config) override { config_.load(config, true); safeSaveAsIni(config_, configFile); }
readAsIni and safeSaveAsIni are helper functions to read/save Configuration object from/to the Fcitx ini-format. The file is saved under $XDG_CONFIG_HOME.