fix(linux): add QA and translation modifier hotkey support to fcitx5 plugin

Extend the fcitx5 plugin with SetQaHotkeyRaw/SetTranslationHotkeyRaw
DBus methods and corresponding signals, so that QA panel toggle and
translation modifier hotkeys work on Linux (not just the main dictation
hotkey). Built-in Shift key translation modifier also handled by the
plugin. Document DBus security model in plugin header.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
aeoformandClaude Opus 4.7 committed 2026-05-16 09:21:38 +08:00
1 parent b349a4dcba
commit 323dbed401
3 files changed
+234 -60

No files matched your search

@@ -7,13 +7,18 @@
*
* DBus 接口: org.fcitx.Fcitx.OpenLess1 (对象路径 /openless)
* 方法:
* CommitText(s: text) — 将文字提交到当前焦点输入上下文
* SetHotkey(as: keys) — 设置听写触发快捷键 (Key::parse 格式)
* SetHotkeyRaw(uu: sym, states) — 直接设 sym+states (不走 parse)
* CommitText(s: text) — 将文字提交到当前焦点输入上下文
* 安全性:本接口在会话总线(session bus)上对同用户
* 所有进程开放,此为 fcitx5/IBus 体系的标准安全模型
* (非特权进程隔离)。
* SetHotkey(as: keys) — 设置听写触发快捷键 (Key::parse 格式)
* SetHotkeyRaw(uu: sym, states) — 直接设听写触发 sym+states (不走 parse)
* SetQaHotkeyRaw(uu: sym, states) — 直接设 QA 面板触发 sym+states
* SetTranslationHotkeyRaw(uu: sym, states) — 直接设翻译模式触发 sym+states
* 信号:
* DictationKeyEvent(uu: sym, states) — 热键被按下
*
* 后续: 当需要 IBus 引擎兼容时 (GNOME),另行实现 org.freedesktop.IBus.Engine。
* DictationKeyEvent(uub: sym, states, isPress) — 听写热键按下/抬起
* QaShortcutEvent(uub: sym, states, isPress) — QA 快捷键按下/抬起
* TranslationModifierEvent(uub: sym, states, isPress) — 翻译修饰键按下/抬起
*/
#include <memory>
@@ -57,6 +62,10 @@ public:
: instance_(instance),
triggerRawSym_(0),
triggerRawStates_(0),
qaRawSym_(0),
qaRawStates_(0),
translationRawSym_(0),
translationRawStates_(0),
savedIc_(nullptr) {
// 1. 读取配置
@@ -91,34 +100,73 @@ public:
auto &keyEvent = static_cast<KeyEvent &>(event);
// 保存当前输入上下文:快捷键按下时用户在目标 app 中,
// 此后胶囊窗口可能抢走焦点,但 commitText 仍能用此 IC 提交文字。
// IC 销毁时通过 InputContextDestroyed 事件自动清空指针(见下方)。
if (!keyEvent.isRelease()) {
savedIc_ = keyEvent.inputContext();
}
// 先检查 raw sym/states(修饰键专用路径,绕过 Key::parse 限制)
auto sym = static_cast<uint32_t>(keyEvent.key().sym());
auto states = static_cast<uint32_t>(keyEvent.key().states());
bool isPress = !keyEvent.isRelease();
// 检查听写触发键(raw + keylist 双路径)
bool dictationMatched = false;
if ((triggerRawSym_ != 0 &&
keyEvent.key().sym() == static_cast<KeySym>(triggerRawSym_) &&
keyEvent.key().states() == static_cast<KeyStates>(triggerRawStates_)) ||
sym == triggerRawSym_ &&
states == triggerRawStates_) ||
(triggerRawSym_ == 0 && [&]() {
for (const auto &hk : triggerKeyList_) {
if (keyEvent.key().sym() == hk.sym() &&
keyEvent.key().states() == hk.states())
if (sym == static_cast<uint32_t>(hk.sym()) &&
states == static_cast<uint32_t>(hk.states()))
return true;
}
return false;
}())) {
auto sym = triggerRawSym_ != 0
dictationMatched = true;
auto dsym = triggerRawSym_ != 0
? triggerRawSym_
: static_cast<uint32_t>(triggerKeyList_[0].sym());
auto states = triggerRawStates_ != 0
auto dstates = triggerRawStates_ != 0
? triggerRawStates_
: static_cast<uint32_t>(triggerKeyList_[0].states());
bool isPress = !keyEvent.isRelease();
FCITX_LOGC(openless, Debug)
<< "Dictation hotkey: sym="
<< sym << " states=" << states
<< "Dictation hotkey: sym=" << dsym
<< " states=" << dstates
<< " isPress=" << isPress;
dictationKeyEvent(sym, states, isPress);
dictationKeyEvent(dsym, dstates, isPress);
keyEvent.filterAndAccept();
return;
}
// 检查 QA 快捷键
if (qaRawSym_ != 0 &&
sym == qaRawSym_ &&
states == qaRawStates_) {
FCITX_LOGC(openless, Debug)
<< "QA shortcut: sym=" << qaRawSym_
<< " states=" << qaRawStates_
<< " isPress=" << isPress;
qaShortcutEvent(qaRawSym_, qaRawStates_, isPress);
keyEvent.filterAndAccept();
return;
}
// 检查翻译模式修饰键(自定义 + 内置 Shift)
bool translationMatched = false;
if (translationRawSym_ != 0 &&
sym == translationRawSym_ &&
states == translationRawStates_) {
translationMatched = true;
}
// 内置 Shift 修饰键
if (sym == 0xffe1 || sym == 0xffe2) {
translationMatched = true;
}
if (translationMatched) {
FCITX_LOGC(openless, Debug)
<< "Translation modifier: sym=" << sym
<< " states=" << states
<< " isPress=" << isPress;
translationModifierEvent(sym, states, isPress);
keyEvent.filterAndAccept();
return;
}
@@ -217,16 +265,44 @@ public:
rebuildTriggerKeys();
}
void setQaHotkeyRaw(uint32_t sym, uint32_t states) {
qaRawSym_ = sym;
qaRawStates_ = states;
RawConfig raw;
readAsIni(raw, configFile());
raw.setValueByPath("QaRawSym", std::to_string(sym));
raw.setValueByPath("QaRawStates", std::to_string(states));
safeSaveAsIni(raw, configFile());
FCITX_LOGC(openless, Info)
<< "SetQaHotkeyRaw: sym=" << sym << " states=" << states;
}
void setTranslationHotkeyRaw(uint32_t sym, uint32_t states) {
translationRawSym_ = sym;
translationRawStates_ = states;
RawConfig raw;
readAsIni(raw, configFile());
raw.setValueByPath("TranslationRawSym", std::to_string(sym));
raw.setValueByPath("TranslationRawStates", std::to_string(states));
safeSaveAsIni(raw, configFile());
FCITX_LOGC(openless, Info)
<< "SetTranslationHotkeyRaw: sym=" << sym << " states=" << states;
}
FCITX_OBJECT_VTABLE_METHOD(commitText, "CommitText", "s", "");
FCITX_OBJECT_VTABLE_METHOD(setHotkey, "SetHotkey", "as", "");
FCITX_OBJECT_VTABLE_METHOD(setHotkeyRaw, "SetHotkeyRaw", "uu", "");
FCITX_OBJECT_VTABLE_METHOD(setQaHotkeyRaw, "SetQaHotkeyRaw", "uu", "");
FCITX_OBJECT_VTABLE_METHOD(setTranslationHotkeyRaw, "SetTranslationHotkeyRaw", "uu", "");
FCITX_OBJECT_VTABLE_SIGNAL(dictationKeyEvent, "DictationKeyEvent", "uub");
FCITX_OBJECT_VTABLE_SIGNAL(qaShortcutEvent, "QaShortcutEvent", "uub");
FCITX_OBJECT_VTABLE_SIGNAL(translationModifierEvent, "TranslationModifierEvent", "uub");
Instance *instance() { return instance_; }
void reloadConfig() override {
readAsIni(config_, configFile());
// 加载原始 sym/states(由 SetHotkeyRaw 写入的持久化键值)
// 加载原始 sym/states(由 SetHotkeyRaw / SetQaHotkeyRaw / SetTranslationHotkeyRaw 写入的持久化键值)
RawConfig raw;
readAsIni(raw, configFile());
{
@@ -237,6 +313,22 @@ public:
auto *v = raw.valueByPath("TriggerRawStates");
triggerRawStates_ = v ? std::stoul(*v, nullptr, 0) : 0;
}
{
auto *v = raw.valueByPath("QaRawSym");
qaRawSym_ = v ? std::stoul(*v, nullptr, 0) : 0;
}
{
auto *v = raw.valueByPath("QaRawStates");
qaRawStates_ = v ? std::stoul(*v, nullptr, 0) : 0;
}
{
auto *v = raw.valueByPath("TranslationRawSym");
translationRawSym_ = v ? std::stoul(*v, nullptr, 0) : 0;
}
{
auto *v = raw.valueByPath("TranslationRawStates");
translationRawStates_ = v ? std::stoul(*v, nullptr, 0) : 0;
}
rebuildTriggerKeys();
}
@@ -264,6 +356,10 @@ private:
KeyList triggerKeyList_;
uint32_t triggerRawSym_;
uint32_t triggerRawStates_;
uint32_t qaRawSym_;
uint32_t qaRawStates_;
uint32_t translationRawSym_;
uint32_t translationRawStates_;
/// 快捷键按下时保存的输入上下文指针,用于 commitText 在失焦后仍能提交文字。
/// 事件处理线程和 DBus 处理线程都是 fcitx5 主事件循环,无竞态。
/// 通过 InputContextDestroyed 事件监听 IC 销毁时自动清空指针。