注意:这是一个正在进行的工作,我在此做一些随笔
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
这里是一些引用链接
- https://blog.csdn.net/czhzasui/article/details/81071383
- https://blog.csdn.net/xiaopei_yan/article/details/81410092
- https://doc.qt.io/qt-5/qdbusmessage.html#createMethodCall
- https://doc.qt.io/qt-5/qdbusinterface.html
- https://thebigdoc.readthedocs.io/en/latest/dbus/dbus.html
Fcitx5 的 DBus 接口
接口和使用定义在了 https://github.com/fcitx/fcitx5-qt 中
通过 D-feet 可以看到 Fcitx5 的 DBus 接口:
除了上面的 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 的流程差不多就是
- 调用 /org/freedesktop/portal/inputmethod/org.fcitx.Fcitx.InputMethod1/CreateInputContext 拿到一个 Object Path
- 在 Object Path 上监听信号,并通过 ProcessKeyEvent 传入按键序列
- 通过 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 一样
信号我还不是很熟练,得先看看了