fcitx5 客户端开发

注意:这是一个正在进行的工作,我在此做一些随笔

fcitx5 提供了程序接口和 dbus 接口。但是程序接口似乎是 glib 相关的,我不太熟悉。虽然 dbus 我也不熟悉,但是我现在学了以后应该还会用到。

第一部分,调用 DBus 函数

囧脸告诉我 DBus 要先调用 CreateInputContext Method 创建一个会话,听起来很简单,但是对于我这个门外汉确实有些难度。

起初我是打算直接用 libdbus,后来换成了 QtDBus。构建工具是 CMake.

https://blog.csdn.net/czhzasui/article/details/81071383 这篇文章介绍了如何使用 QDBusConnection 调用 DBus 的 Method。

QDBusMessage QDBusMessage:: createMethodCall (const QString & service , const QString & path , const QString & interface , const QString & method )

Fcitx5 初始的 DBus 名字叫 org.fcitx.Fcitx5,Method 的路径在 freedesktop/IBus/org.freedesktop.IBus/

刚开始是用的 QDBusViewer 这个东西,但是调用很方便,用起来很麻烦。后来用 gdbus introspect -e -d org.fcitx.Fcitx5 -o /org/freedesktop/IBus 进行尝试拿到了前两个参数,最后发现 D-feet 真是个好东西。。。双击一下三个参数都有了。

QDBusViewer 的好处是调用后可以观察到返回值。我这里观察到(配合 qDebug)返回值类型是 QDBusObjectPath。所以有这样的代码:

#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusObjectPath>
#include <QDebug>
#include <QList>
#include <QTextStream>
#include <QVariant>

int main(int argc, char **argv) {
    QDBusMessage msg = QDBusMessage::createMethodCall("org.fcitx.Fcitx5", "/org/freedesktop/IBus",
                                                      "org.freedesktop.IBus", "CreateInputContext");
    msg << QStringLiteral("sss");
    auto res = QDBusConnection::sessionBus().call(msg);
    if(res.type() == QDBusMessage::ReplyMessage) {
        QDBusObjectPath path = res.arguments().takeFirst().value<QDBusObjectPath>();
        qDebug() << path.path();
    } else {
        qDebug() << "error";
    }
    return 0;
}

在 Fcitx5 启动的情况下应当会输出一个路径,形如 InputContext_x 的形式,x是一个数字

拿到的数据可以在 org.fcitx.Fcitx5 的子菜单 freedesktop/IBus/InputContenx_x 看到。这里可以看到此对象提供的方法

说起来有些奇怪,昨天晚上我在看 DBus 接口的时候发现 processKeyEent 是一个 signal,而且参数只有 3 个,与 xml 中声明的不一样。今天早上再看发现又变成 Method 了,而且参数也一样,不知道到底什么情况

囧脸说 fcitx5 的 DBus 接口是基于按键序列的,而不是基于字符序列(和补全不同),因此主要任务应该是找到相关接口,然后看看接口怎么用。

DBus 的接口是放到了 /org/freedesktop/portal/inputcontext/1 中的 org.fcitx.Fcitx.InputContext1 中,其中末尾的数字是不定的。

里面有一个 Method,签名为:

ProcessKeyEvent (UInt32 arg_0, UInt32 arg_1, UInt32 arg_2, Boolean arg_3, UInt32 arg_4) ↦ (Boolean arg_5)

https://github.com/fcitx/fcitx5-qt/blob/master/qt5/dbusaddons/interfaces/org.fcitx.Fcitx.InputContext1.xml 中可以看到 DBus 定义,但是在 https://github1s.com/fcitx/fcitx5-qt/blob/master/qt5/dbusaddons/fcitxqtinputcontextproxy.h#L38 可以看到一个更简洁的接口:

QDBusPendingReply<bool> processKeyEvent(unsigned int keyval,
                                            unsigned int keycode,
                                            unsigned int state, bool type,
                                            unsigned int time)

https://github1s.com/fcitx/fcitx5-qt/blob/master/qt5/platforminputcontext/qfcitxplatforminputcontext.cpp#L831 中可以看到使用此函数的代码,大概为:


bool QFcitxPlatformInputContext::filterEvent(const QEvent *event) {
    do { // 这里是一个一次性的循环,我不知道意图为何
        if (event->type() != QEvent::KeyPress &&
            event->type() != QEvent::KeyRelease) {
            break;
        }// 使用 一次性的 do-while 应该是为了跳出循环方便,这样 return 写起来方便

        const QKeyEvent *keyEvent = static_cast<const QKeyEvent *>(event);
        quint32 keyval = keyEvent->nativeVirtualKey(); // 第一个参数
        quint32 keycode = keyEvent->nativeScanCode();// 第二个参数
        quint32 state = keyEvent->nativeModifiers(); // 第三个参数
        bool isRelease = keyEvent->type() == QEvent::KeyRelease; // 第四个参数

        if (!inputMethodAccepted() && !objectAcceptsInputMethod())
            break;

        QObject *input = qApp->focusObject();

        if (!input) {
            break;
        }

        FcitxQtInputContextProxy *proxy = validICByWindow(qApp->focusWindow());

        if (!proxy) {
            if (filterEventFallback(keyval, keycode, state, isRelease)) {
                return true;
            } else {
                break;
            }
        }

        update(Qt::ImHints);
        proxy->focusIn(); // 调用 DBus 接口

        auto stateToFcitx = state;
        if (keyEvent->isAutoRepeat()) {// 检查是否是 autorepeat key。这个键没用过,只听说过,似乎是自动点击?
            // KeyState::Repeat
            stateToFcitx |= (1u << 31);
        }
        auto reply = proxy->processKeyEvent(keyval, keycode, stateToFcitx,
                                            isRelease, keyEvent->timestamp());

        if (Q_UNLIKELY(syncMode_)) {
            reply.waitForFinished();

            if (reply.isError() || !reply.value()) {
                if (filterEventFallback(keyval, keycode, state, isRelease)) {
                    return true;
                } else {
                    break;
                }
            } else {
                update(Qt::ImCursorRectangle);
                return true;
            }
        } else {
            ProcessKeyWatcher *watcher = new ProcessKeyWatcher(
                *keyEvent, qApp->focusWindow(), reply, proxy);
            connect(watcher, &QDBusPendingCallWatcher::finished, this,
                    &QFcitxPlatformInputContext::processKeyEventFinished);
            return true;
        }
    } while (0);
    return QPlatformInputContext::filterEvent(event);
}

所以这个接口的五个参数分别是:

  • keyval:键盘虚拟码:QKeyEvent::nativeVirtualKey()
  • keycode:扫描码,可以映射到虚拟码:QKeyEvent::nativeScanCode()
  • state:修饰键:QKeyEvent::modifiers()
  • type:isReleased: keyEvent->type() == QEvent::KeyRelease
  • time:时间戳: QKeyEvent::timestamp() 这个成员函数实际上我并没有发现

上面 DBus 的接口在 https://github1s.com/fcitx/fcitx5-qt/blob/master/qt5/platforminputcontext/qfcitxplatforminputcontext.h#L143-L144 中都有对应的实现,实现的类为 QFcitxPlatformInputContext

这里是一些引用链接

Fcitx5 的 DBus 接口

接口和使用定义在了 https://github.com/fcitx/fcitx5-qt

通过 D-feet 可以看到 Fcitx5 的 DBus 接口:

图片.png

除了上面的 ProcessKeyEvent 和 无参函数外,其他函数的含义为:

Methods:

inline QDBusPendingReply<> SelectCandidate(int index)
    {
        QList<QVariant> argumentList;
        argumentList << QVariant::fromValue(index);
        return asyncCallWithArgumentList(QStringLiteral("SelectCandidate"), argumentList);
    }

此函数定义在 fcitxqtcontextproxyimpl.h 中。可以看到参数的名字为 index

其他函数的调用类似

// fcitxqtinputcontextproxy.h
QDBusPendingReply<> setCapability(qulonglong caps);
// fcitxflags.h
enum FcitxCapabilityFlag : uint64_t {
    FcitxCapabilityFlag_Preedit = (1 << 1),
    FcitxCapabilityFlag_Password = (1 << 3),
    FcitxCapabilityFlag_FormattedPreedit = (1 << 4),
    FcitxCapabilityFlag_ClientUnfocusCommit = (1 << 5),
    FcitxCapabilityFlag_SurroundingText = (1 << 6),
    FcitxCapabilityFlag_Email = (1 << 7),
    FcitxCapabilityFlag_Digit = (1 << 8),
    FcitxCapabilityFlag_Uppercase = (1 << 9),
    FcitxCapabilityFlag_Lowercase = (1 << 10),
    FcitxCapabilityFlag_NoAutoUpperCase = (1 << 11),
    FcitxCapabilityFlag_Url = (1 << 12),
    FcitxCapabilityFlag_Dialable = (1 << 13),
    FcitxCapabilityFlag_Number = (1 << 14),
    FcitxCapabilityFlag_NoSpellCheck = (1 << 17),
    FcitxCapabilityFlag_Alpha = (1 << 21),
    FcitxCapabilityFlag_GetIMInfoOnFocus = (1 << 23),
    FcitxCapabilityFlag_RelativeRect = (1 << 24),

    FcitxCapabilityFlag_Multiline = (1ull << 35),
    FcitxCapabilityFlag_Sensitive = (1ull << 36),
    FcitxCapabilityFlag_KeyEventOrderFix = (1ull << 37),
    FcitxCapabilityFlag_ReportKeyRepeat = (1ull << 38),
    FcitxCapabilityFlag_ClientSideInputPanel = (1ull << 39),
};
// fcitxqtinputcontextproxyimpl.h
inline QDBusPendingReply<> SetSurroundingText(const QString &text, uint cursor, uint anchor)
    {
        QList<QVariant> argumentList;
        argumentList << QVariant::fromValue(text) << QVariant::fromValue(cursor) << QVariant::fromValue(anchor);
        return asyncCallWithArgumentList(QStringLiteral("SetSurroundingText"), argumentList);
    }

这个函数不知道是干什么的

// fcitxqtinputcontextproxy.h
QDBusPendingReply<> setSurroundingTextPosition(unsigned int cursor,
                                                   unsigned int anchor);

下面是信号部分。这部分也是 Qt 的 Q_SIGNALS

    void commitString(const QString &str);
    void currentIM(const QString &name, const QString &uniqueName,
                   const QString &langCode);
    void deleteSurroundingText(int offset, unsigned int nchar);
    void forwardKey(unsigned int keyval, unsigned int state, bool isRelease);
    void updateFormattedPreedit(const FcitxQtFormattedPreeditList &str,
                                int cursorpos);
    void updateClientSideUI(const FcitxQtFormattedPreeditList &preedit,
                            int cursorpos,
                            const FcitxQtFormattedPreeditList &auxUp,
                            const FcitxQtFormattedPreeditList &auxDown,
                            const FcitxQtStringKeyValueList &candidates,
                            int candidateIndex, int layoutHint, bool hasPrev,
                            bool hasNext);
    void inputContextCreated(const QByteArray &uuid);

下面是一个完整的接口定义

class FCITX5QT5DBUSADDONS_EXPORT FcitxQtInputContextProxy : public QObject {
    Q_OBJECT
public:
    FcitxQtInputContextProxy(FcitxQtWatcher *watcher, QObject *parent);
    ~FcitxQtInputContextProxy();

    bool isValid() const;
    void setDisplay(const QString &display);
    const QString &display() const;

public Q_SLOTS:
    QDBusPendingReply<> focusIn();
    QDBusPendingReply<> focusOut();
    QDBusPendingReply<bool> processKeyEvent(unsigned int keyval,
                                            unsigned int keycode,
                                            unsigned int state, bool type,
                                            unsigned int time);
    QDBusPendingReply<> reset();
    QDBusPendingReply<> setCapability(qulonglong caps);
    QDBusPendingReply<> setCursorRect(int x, int y, int w, int h);
    QDBusPendingReply<> setCursorRectV2(int x, int y, int w, int h,
                                        double scale);
    QDBusPendingReply<> setSurroundingText(const QString &text,
                                           unsigned int cursor,
                                           unsigned int anchor);
    QDBusPendingReply<> setSurroundingTextPosition(unsigned int cursor,
                                                   unsigned int anchor);
    QDBusPendingReply<> prevPage();
    QDBusPendingReply<> nextPage();
    QDBusPendingReply<> selectCandidate(int i);

Q_SIGNALS:
    void commitString(const QString &str);
    void currentIM(const QString &name, const QString &uniqueName,
                   const QString &langCode);
    void deleteSurroundingText(int offset, unsigned int nchar);
    void forwardKey(unsigned int keyval, unsigned int state, bool isRelease);
    void updateFormattedPreedit(const FcitxQtFormattedPreeditList &str,
                                int cursorpos);
    void updateClientSideUI(const FcitxQtFormattedPreeditList &preedit,
                            int cursorpos,
                            const FcitxQtFormattedPreeditList &auxUp,
                            const FcitxQtFormattedPreeditList &auxDown,
                            const FcitxQtStringKeyValueList &candidates,
                            int candidateIndex, int layoutHint, bool hasPrev,
                            bool hasNext);
    void inputContextCreated(const QByteArray &uuid);

private:
    FcitxQtInputContextProxyPrivate *const d_ptr;
    Q_DECLARE_PRIVATE(FcitxQtInputContextProxy);
};

FcitxQtFormattedPreeditList 的定义我没找到,但是应该是这样的

struct FcitxQtFormattedPreeditList{
QString;
int32;
}

第一个参数应该是字,第二个参数应该是 pos

这样一来,fcitx 的流程差不多就是

  1. 调用 /org/freedesktop/portal/inputmethod/org.fcitx.Fcitx.InputMethod1/CreateInputContext 拿到一个 Object Path
  2. 在 Object Path 上监听信号,并通过 ProcessKeyEvent 传入按键序列
  3. 通过 UpdateClientSideUI 拿到数据

对于第一个函数,我发现了两种调用方式,一种是无参调用,第二种是:

        QFileInfo info(QCoreApplication::applicationFilePath());
        portal_ = true;
        improxy_ = new FcitxQtInputMethodProxy(
            owner, "/org/freedesktop/portal/inputmethod", connection, q);
        FcitxQtStringKeyValueList list;
        FcitxQtStringKeyValue arg;
        arg.setKey("program");
        arg.setValue(info.fileName());
        list << arg;
        if (!display_.isEmpty()) {
            FcitxQtStringKeyValue arg2;
            arg2.setKey("display");
            arg2.setValue(display_);
            list << arg2;
        }

        auto result = improxy_->CreateInputContext(list);

display_ 是一个 QString 类型,我猜应该是和 $DISPLAY 一样

信号我还不是很熟练,得先看看了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容