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:
1 parent
b349a4dcba
commit
323dbed401
3 files changed
+234
-60
No files matched your search
@@ -1221,9 +1221,11 @@ mod platform {
|
||||
|
||||
fn update_modifier_shortcuts(
|
||||
&self,
|
||||
_qa_trigger: Option<HotkeyTrigger>,
|
||||
_translation_trigger: Option<HotkeyTrigger>,
|
||||
qa_trigger: Option<HotkeyTrigger>,
|
||||
translation_trigger: Option<HotkeyTrigger>,
|
||||
) {
|
||||
crate::linux_fcitx::sync_qa_binding(qa_trigger);
|
||||
crate::linux_fcitx::sync_translation_binding(translation_trigger);
|
||||
}
|
||||
|
||||
fn reset_held_state(&self) {}
|
||||
|
||||
@@ -56,39 +56,108 @@ pub fn set_hotkey_raw(sym: u32, states: u32) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// X11 keysym 值(用于 SetHotkeyRaw,绕过 Key::parse 的修饰键限制)。
|
||||
/// 通过 fcitx5 插件设置 QA 面板快捷键 sym + states。
|
||||
pub fn set_qa_hotkey_raw(sym: u32, states: u32) -> Result<(), String> {
|
||||
let conn = dbus::blocking::Connection::new_session()
|
||||
.map_err(|e| format!("dbus session: {e}"))?;
|
||||
let msg = dbus::Message::new_method_call(DEST, PATH, IFACE, "SetQaHotkeyRaw")
|
||||
.map_err(|e| format!("build msg: {e}"))?
|
||||
.append2(sym, states);
|
||||
conn.send_with_reply_and_block(msg, TIMEOUT)
|
||||
.map_err(|e| format!("SetQaHotkeyRaw: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 通过 fcitx5 插件设置翻译模式修饰键 sym + states。
|
||||
pub fn set_translation_hotkey_raw(sym: u32, states: u32) -> Result<(), String> {
|
||||
let conn = dbus::blocking::Connection::new_session()
|
||||
.map_err(|e| format!("dbus session: {e}"))?;
|
||||
let msg = dbus::Message::new_method_call(DEST, PATH, IFACE, "SetTranslationHotkeyRaw")
|
||||
.map_err(|e| format!("build msg: {e}"))?
|
||||
.append2(sym, states);
|
||||
conn.send_with_reply_and_block(msg, TIMEOUT)
|
||||
.map_err(|e| format!("SetTranslationHotkeyRaw: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// X11 keysym 值(用于 SetHotkeyRaw / SetQaHotkeyRaw / SetTranslationHotkeyRaw,
|
||||
/// 绕过 Key::parse 的修饰键限制)。
|
||||
const KEYSYM_CONTROL_R: u32 = 0xffe4;
|
||||
const KEYSYM_CONTROL_L: u32 = 0xffe3;
|
||||
const KEYSYM_ALT_R: u32 = 0xffea;
|
||||
const KEYSYM_ALT_L: u32 = 0xffe9;
|
||||
const KEYSYM_SUPER_R: u32 = 0xffec;
|
||||
const KEYSYM_SUPER_L: u32 = 0xffeb;
|
||||
const KEYSYM_SHIFT_R: u32 = 0xffe2;
|
||||
const KEYSYM_SHIFT_L: u32 = 0xffe1;
|
||||
|
||||
/// 将 OpenLess 的热键绑定同步到 fcitx5 插件。
|
||||
///
|
||||
/// 把 `HotkeyTrigger`(如 RightControl)转换为 fcitx5 keysym,
|
||||
/// 通过 `SetHotkeyRaw` 配置插件(绕过 fcitx5 Key 类对纯修饰键的限制)。
|
||||
/// 将 HotkeyTrigger 转换为 X11 keysym。
|
||||
fn trigger_to_keysym(trigger: crate::types::HotkeyTrigger) -> u32 {
|
||||
match trigger {
|
||||
crate::types::HotkeyTrigger::RightControl => KEYSYM_CONTROL_R,
|
||||
crate::types::HotkeyTrigger::LeftControl => KEYSYM_CONTROL_L,
|
||||
crate::types::HotkeyTrigger::RightOption | crate::types::HotkeyTrigger::RightAlt => KEYSYM_ALT_R,
|
||||
crate::types::HotkeyTrigger::LeftOption => KEYSYM_ALT_L,
|
||||
crate::types::HotkeyTrigger::RightCommand => KEYSYM_SUPER_R,
|
||||
crate::types::HotkeyTrigger::Fn => KEYSYM_SUPER_L,
|
||||
crate::types::HotkeyTrigger::Custom => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn trigger_name(trigger: crate::types::HotkeyTrigger) -> &'static str {
|
||||
match trigger {
|
||||
crate::types::HotkeyTrigger::RightControl => "Control_R",
|
||||
crate::types::HotkeyTrigger::LeftControl => "Control_L",
|
||||
crate::types::HotkeyTrigger::RightOption | crate::types::HotkeyTrigger::RightAlt => "Alt_R",
|
||||
crate::types::HotkeyTrigger::LeftOption => "Alt_L",
|
||||
crate::types::HotkeyTrigger::RightCommand => "Super_R",
|
||||
crate::types::HotkeyTrigger::Fn => "Super_L",
|
||||
crate::types::HotkeyTrigger::Custom => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 OpenLess 的主听写热键绑定同步到 fcitx5 插件。
|
||||
pub fn sync_binding_to_plugin(binding: &crate::types::HotkeyBinding) {
|
||||
if binding.trigger == crate::types::HotkeyTrigger::Custom {
|
||||
return;
|
||||
}
|
||||
let (sym, name) = match binding.trigger {
|
||||
crate::types::HotkeyTrigger::RightControl => (KEYSYM_CONTROL_R, "Control_R"),
|
||||
crate::types::HotkeyTrigger::LeftControl => (KEYSYM_CONTROL_L, "Control_L"),
|
||||
crate::types::HotkeyTrigger::RightOption | crate::types::HotkeyTrigger::RightAlt => {
|
||||
(KEYSYM_ALT_R, "Alt_R")
|
||||
}
|
||||
crate::types::HotkeyTrigger::LeftOption => (KEYSYM_ALT_L, "Alt_L"),
|
||||
crate::types::HotkeyTrigger::RightCommand => (KEYSYM_SUPER_R, "Super_R"),
|
||||
crate::types::HotkeyTrigger::Fn => (KEYSYM_SUPER_L, "Super_L"),
|
||||
crate::types::HotkeyTrigger::Custom => unreachable!(),
|
||||
};
|
||||
let sym = trigger_to_keysym(binding.trigger);
|
||||
let name = trigger_name(binding.trigger);
|
||||
match set_hotkey_raw(sym, 0) {
|
||||
Ok(()) => log::info!("[fcitx] Synced hotkey {name} (sym={sym}) to plugin via SetHotkeyRaw"),
|
||||
Err(e) => log::warn!("[fcitx] Failed to sync hotkey to plugin: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 QA 面板快捷键同步到 fcitx5 插件。
|
||||
pub fn sync_qa_binding(trigger: Option<crate::types::HotkeyTrigger>) {
|
||||
let Some(trigger) = trigger else {
|
||||
// 无 QA 快捷键时清空插件端配置
|
||||
let _ = set_qa_hotkey_raw(0, 0);
|
||||
return;
|
||||
};
|
||||
let sym = trigger_to_keysym(trigger);
|
||||
let name = trigger_name(trigger);
|
||||
match set_qa_hotkey_raw(sym, 0) {
|
||||
Ok(()) => log::info!("[fcitx] Synced QA hotkey {name} (sym={sym}) to plugin via SetQaHotkeyRaw"),
|
||||
Err(e) => log::warn!("[fcitx] Failed to sync QA hotkey to plugin: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// 将翻译模式快捷键同步到 fcitx5 插件。
|
||||
pub fn sync_translation_binding(trigger: Option<crate::types::HotkeyTrigger>) {
|
||||
let Some(trigger) = trigger else {
|
||||
let _ = set_translation_hotkey_raw(0, 0);
|
||||
return;
|
||||
};
|
||||
let sym = trigger_to_keysym(trigger);
|
||||
let name = trigger_name(trigger);
|
||||
match set_translation_hotkey_raw(sym, 0) {
|
||||
Ok(()) => log::info!("[fcitx] Synced translation hotkey {name} (sym={sym}) to plugin via SetTranslationHotkeyRaw"),
|
||||
Err(e) => log::warn!("[fcitx] Failed to sync translation hotkey to plugin: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// 快速检查 fcitx5 OpenLess 插件是否可用(DBus 对象存在)。
|
||||
pub fn available() -> bool {
|
||||
let conn = match dbus::blocking::Connection::new_session() {
|
||||
@@ -110,10 +179,6 @@ pub fn available() -> bool {
|
||||
/// 本函数将此信号转发为 `HotkeyEvent::Pressed` / `Released` 到协调器事件通道。
|
||||
///
|
||||
/// 后台线程在 `tx` 全部 drop(协调器关闭)或 DBus 连接断开时自动退出。
|
||||
///
|
||||
/// # 调用时机
|
||||
///
|
||||
/// Linux 热键源:通过 DBus 监听 fcitx5 插件的 DictationKeyEvent 信号。
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn start_dictation_signal_listener(
|
||||
tx: std::sync::mpsc::Sender<crate::hotkey::HotkeyEvent>,
|
||||
@@ -131,10 +196,10 @@ pub fn start_dictation_signal_listener(
|
||||
}
|
||||
};
|
||||
|
||||
// 同时监听所有三个信号
|
||||
let rule = match dbus::message::MatchRule::parse(
|
||||
"type='signal',\
|
||||
interface='org.fcitx.Fcitx.OpenLess1',\
|
||||
member='DictationKeyEvent'",
|
||||
interface='org.fcitx.Fcitx.OpenLess1'",
|
||||
) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
@@ -143,24 +208,35 @@ pub fn start_dictation_signal_listener(
|
||||
}
|
||||
};
|
||||
|
||||
// 信号参数: (sym: u32, states: u32, is_press: bool)
|
||||
let match_result = conn.add_match(rule, move |args: (u32, u32, bool), _conn, _msg| {
|
||||
let (_sym, _states, is_press) = args;
|
||||
let tx2 = tx.clone();
|
||||
let _match = match conn.add_match(rule, move |args: (u32, u32, bool), _conn, msg| {
|
||||
let (sym, states, is_press) = args;
|
||||
let member = msg.member();
|
||||
let member_str: String = member.as_ref().map(|m| m.to_string()).unwrap_or_default();
|
||||
log::debug!(
|
||||
"[fcitx-hotkey] DictationKeyEvent: sym={}, states={}, isPress={}",
|
||||
_sym,
|
||||
_states,
|
||||
is_press,
|
||||
"[fcitx-hotkey] Signal {}: sym={}, states={}, isPress={}",
|
||||
member_str, sym, states, is_press,
|
||||
);
|
||||
let event = if is_press {
|
||||
crate::hotkey::HotkeyEvent::Pressed
|
||||
} else {
|
||||
crate::hotkey::HotkeyEvent::Released
|
||||
};
|
||||
let _ = tx.send(event);
|
||||
true // 保持匹配活跃
|
||||
});
|
||||
let _match = match match_result {
|
||||
if let Some(member) = member {
|
||||
if member == "DictationKeyEvent" {
|
||||
let event = if is_press {
|
||||
crate::hotkey::HotkeyEvent::Pressed
|
||||
} else {
|
||||
crate::hotkey::HotkeyEvent::Released
|
||||
};
|
||||
let _ = tx.send(event);
|
||||
} else if member == "QaShortcutEvent" {
|
||||
if is_press {
|
||||
let _ = tx2.send(crate::hotkey::HotkeyEvent::QaShortcutPressed);
|
||||
}
|
||||
} else if member == "TranslationModifierEvent" {
|
||||
if is_press {
|
||||
let _ = tx2.send(crate::hotkey::HotkeyEvent::TranslationModifierPressed);
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
log::warn!("[fcitx-hotkey] Failed to add match: {e}");
|
||||
@@ -168,7 +244,7 @@ pub fn start_dictation_signal_listener(
|
||||
}
|
||||
};
|
||||
|
||||
log::info!("[fcitx-hotkey] Listening for DictationKeyEvent signals");
|
||||
log::info!("[fcitx-hotkey] Listening for OpenLess1 signals");
|
||||
loop {
|
||||
if let Err(e) = conn.process(Duration::from_millis(500)) {
|
||||
log::warn!("[fcitx-hotkey] DBus process error: {e}");
|
||||
|
||||
@@ -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 销毁时自动清空指针。
|
||||
|
||||
Reference in new issue
Block a user