Develop an simple input method

From Fcitx
Revision as of 03:11, 19 April 2023 by Matrikslee (talk | contribs) (Marked this version for translation)
Jump to navigation Jump to search
Other languages:

This is a step-by-step instruction for writing a Fcitx 5 input method. The same instruction can be used for developing other types of addons, just input method engine is the most complex ones.

Understand the file structure for a Fcitx shared library addon

Fcitx 5 provides a extensible framework for adding new addon types, but shared library support is built in and is the base of all other addon types. So we will only cover shared library addon in this document.

[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

Above is the file structure for an input method addon. For other types of addons, files under inputmethod/ is not needed. The file name of [addon name].conf matters and will be used to uniquely reference this specific addon. Fcitx also follows XDG directory standard, so the files under XDG_DATA_DIR/fcitx5 will also be checked. Similarly, file name of configuration file under inputmethod/ also matters and will be the unique name of a certain input method.

Example of [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

Example of [input method name].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

The file is in a ini-like format, with certain fcitx specific extensions and rules. It also supports XDG Desktop file style I18N for translation.

Use CMake build system

It is your own freedom to pick whatever build system you would d like to use, as long as you produce the correct files. Fcitx 5 provides lots of support with CMake, so using CMake would be the most convenient way to build a Fcitx project. In this document, we will only cover using CMake as build system.

A quick start: Quwei

Quwei input method is an input method that basically allow you type the digit of GB2312 and produce the Chinese Character that matches this code. It used to be supported by Fcitx 4, but not included by Fcitx 5 anymore. Though it is hard to use, it can serve as a good example on how to implement a simple input method for Fcitx 5.

Project skeleton

So let we start with a skeleton of this project.

├── 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

You may want to check some CMake tutorial to understand some basic CMake usage.

The CMakeLists.txt in the root directory lookup for the dependency.

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)

The src/CMakeLists.txt would looks like

# 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 would look like

[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

This file will be loaded as an InputMethodEntry object.

quwei-addon.conf.in looks like

[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


This file will be loaded as an AddonInfo object.

Basic implementation of InputMethodEngine

You may refer to the code at the first commit in the github.

Version 1 of 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_

Version 1 of 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);

When implement a Fcitx addon, it should be a sub-class of AddonInstance. The instantiation of AddonInstance is done via a AddonFactory. InputMethodEngineV2 is a sub-class of AddonInstance. This class needs to be used when implementing an input method addon.

The minimum implantation of an input method engine only need to contain the implementation of the keyEvent function.

Here, we use a iostream like macro FCITX_INFO() to write every key we pressed to the log.

Assume the prefix your fcitx installation is /usr. The command to build this project would be:

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

If everything works fine, the install command should print out something like:

-- Install configuration: "Debug"
-- Installing: /usr/lib/fcitx5/quwei.so
-- Installing: /usr/share/fcitx5/addon/quwei.conf
-- Installing: /usr/share/fcitx5/inputmethod/quwei.conf

Now you can restart fcitx5 with fcitx5 -rd and add Quwei to the your configuration with Configtool (Fcitx 5).

After switching to Quwei, the key press in application will make Fcitx 5 print out something like:

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

So you know your input method engine is now working.

Implement Input Method logic

Quwei is basically typing 4 digit number of Quwei code. Quwei code can be treated as Qu code xx and Wei code yy. The mapping from Quwei to GB2312 is (0xA0 + Qu, 0xA0 + Wei). When user type 3 digits of Quwei, input method will display a candidate list of 10 possible character with given quwei prefix.

You may refer to the code at the second commit in the github.

Store the state for different input contexts

Fcitx allows different input context to held a different state. The state usually refers to the partially typed text and all other associated data structures. In Quwei case, the state is the digits that user already typed. To represent this, Fcitx provides a convenient class InputBuffer to allow engine to use this class conveniently to edit the internal state. In order to automatically construct the state when an input context getting constructed, Fcitx provides a framework called InputContextProperty. In order to use this, you first need to register a factory class to InputContextManager, via registerProperty. Each property need to have a global unique name. The name can be something human-understandable. In Quwei case, I just use "quweiState". The benefit of using a nicer name is just in case you're developing something cross addon (Another addon need to access certain internal state of this addon), you can use this common name to load it in a different addon. If you do not need access from outside, the name does not really matter.

The factory class comes with a handy C++ template, FactoryFor. This is actually an type alias to LambdaInputContextPropertyFactory. This class simply accepts a lambda function as the factory implementation. This can save you sometime from creating your own subclass of 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.