fix: respond to TSF conversion mode compartment changes#1889
Conversation
- Read TF_CONVERSIONMODE_NATIVE flag from the compartment on GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION change - Compare with current _status.ascii_mode and toggle via _HandleLangBarMenuSelect when they differ - Use the same IPC path (TrayCommand → SetOption) as the Shift key for consistent behavior - Clear pending composition on mode switch to avoid stale preedit Closes rime#1371
- Replace reading compartment value and comparing with _status.ascii_mode by a blind toggle, matching the OPENCLOSE handler's Shift-key path. The OnChange notification may deliver stale values, making the read-compare approach unreliable. - Remove _IsKeyboardOpen() guard; call _SetKeyboardOpen(true) to ensure the keyboard is active when processing external changes. - Add _updatingLanguageBar re-entrancy guard in _UpdateLanguageBar to prevent _SetCompartmentDWORD from re-triggering the handler. - Add _SetKeyboardOpen/_EnableLanguageBar calls to match the OPENCLOSE handler's else branch.
|
fix: replace read-compare with blind toggle for conversion compartment
|
修复分析:WeaselTSF 响应 TSF 转换模式 Compartment 外部变更问题通过 TSF compartment 程序化切换中英文模式(如 行为变化
架构背景WeaselTSF 是 TSF Text Service(实现 根因根因有两个层面:源代码逻辑缺陷 和 DLL 部署位置。两者必须同时解决,缺一不可。 1. 源代码:CONVERSION handler 撤销外部变更
// 原始代码 (commit 93eec2d)
} else if (IsEqualGUID(guidCompartment,
GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) {
BOOL isOpen = _IsKeyboardOpen();
if (isOpen) {
weasel::ResponseParser parser(NULL, NULL, &_status, NULL,
&_cand->style());
bool ok = m_client.GetResponseData(std::ref(parser)); // 查询 RIME 后端当前状态
_UpdateLanguageBar(_status); // 写回 compartment
}
}问题链条:
原始代码从不读取 compartment 值来判断外部请求的目标模式,也不调用 2. DLL 部署位置WeaselTSF 的 CLSID
TSF 框架从上述系统路径加载 DLL。如果将修复后的 DLL 部署到其他路径(如 修复方案源代码修改修改文件: 将 CONVERSION handler 从"查询后端 → 写回 compartment"改为"toggle → 通知 RIME → 同步 UI",与 // 修复后
} else if (IsEqualGUID(guidCompartment,
GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) {
if (_updatingLanguageBar)
return S_OK;
_status.ascii_mode = !_status.ascii_mode;
_SetKeyboardOpen(true);
if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled())
_EnableLanguageBar(true);
_HandleLangBarMenuSelect(_status.ascii_mode
? ID_WEASELTRAY_ENABLE_ASCII
: ID_WEASELTRAY_DISABLE_ASCII);
if (_pEditSessionContext)
m_client.ClearComposition();
_UpdateLanguageBar(_status);
}与原始代码的关键差异:
重入守卫
// LanguageBar.cpp
void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) {
// ...
_updatingLanguageBar = true;
_SetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION);
_updatingLanguageBar = false;
// ...
}// WeaselTSF.h
BOOL _updatingLanguageBar = false;为什么用 blind toggle 而非读取 compartment 值曾尝试读取 compartment 值来推导目标模式( 外部工具(如 im-control)只在 验证测试环境
测试结果调试日志验证在 链路完整:hook 写入 compartment → sink 触发 → toggle ascii_mode → 通知 RIME 引擎 → 相关链接
|
|
不用管_IsKeyboardOpen? |
|
按照我之前测试结果,实际上微软拼音/微软日文输入法也不响应 |
|
因为目前有两种可能的状态,一个是开关键盘的状态,一个是切换ascii的状态。 你的修改上没有考虑输入法是不是开启,感觉有洞 |
|
期待~ |
感谢回复。我想确认一下你说的”输入法是不是开启“具体指哪种情况:
|
|
原来旧的weasel的响应 打开/关闭输入法 消息是执行的打开keyboard和关闭keyboard,对应影响的是_IsKeyboardOpen 的状态;后来改了默认切换ascii_mode,可选toggleime |
- Realign _HandleLangBarMenuSelect ternary to one-line condition with colon continuation aligned under the question mark, per Chromium clang-format style; resolves CI lint violations at lines 275-276
感谢澄清历史背景,现在理解了 OPENCLOSE 语义的演变。 基于你的说明,我梳理了当前 PR 的设计,确认在 _isToOpenClose=false(默认 Shift 切换模式)下是正确的: 当前PR的设计:CONVERSION handler 中 _SetKeyboardOpen(true) 的调用,在 im-control 的典型工作流里是 no-op——因为外部工具总是先写 OPENCLOSE=1 启用 RIME,再写 CONVERSION 切换中英。到 CONVERSION handler 执行时 _IsKeyboardOpen() 已为 true,SetValue(TRUE) 不改变值、不触发 OnChange,OPENCLOSE handler 不会被调用。经实测,gvim 和 terminal vim 下进入/离开 insert mode 均工作正常:进入时切英文、离开时恢复英文、全程不会误禁用 RIME。 一个可选的优化方向:从 CONVERSION handler 中移除 _SetKeyboardOpen(true),让两个 compartment 各司其职——OPENCLOSE 管启用/禁用,CONVERSION 管中英切换。这样在 _isToOpenClose=false 模式下不会依赖已被重新定义语义的 OPENCLOSE compartment。但这需要外部调用方(如 im-control)保证先 -k open 再 -c,否则单独写 CONVERSION 不会启用 RIME。 考虑到当前实现已经正确工作且对调用方更宽容(即使单独写 CONVERSION 也会自动启用 RIME),我倾向保持现状。您认为呢? PS:上述提到的正确工作,其环境包括: |
…ndows 11 compatibility blind toggle (_status.ascii_mode = !_status.ascii_mode) relies on the _updatingLanguageBar boolean guard catching synchronous OnChange re-entrancy from _UpdateLanguageBar. On Windows 11, TSF may deliver OnChange asynchronously or inject extra compartment writes via TextInputHost.exe, causing the guard to miss and producing an odd number of toggles (wrong mode). Replace with reading the actual compartment value and comparing against _status.ascii_mode. This is naturally idempotent: re-entrant or delayed OnChange reads the value written by _UpdateLanguageBar, finds it matches current state, and skips. The _updatingLanguageBar guard remains as a first layer. Also update analysis doc with Windows 11 root cause and fix rationale.
Restore conversion-compartment-fix-analysis.md to its original state (documenting the blind-toggle approach for Windows 10). Create new windows11-compatibility-fix.md for the value-driven fix that addresses Windows 11 TSF async callback behavior.
Update windows11-compatibility-fix.md with the finding that Windows 11 TSF only triggers OnChange for writes from activated clients. im-control was using TF_CLIENTID_NULL, so OnChange was never fired.
…teLanguageBar Log compartment value, desiredAsciiMode, _status.ascii_mode, _updatingLanguageBar state, and decision (skip/process) in _HandleCompartment. Log flags and _updatingLanguageBar state in _UpdateLanguageBar. Use DebugView to diagnose Windows 11 behavior.
…ingW Add _DbgInit/_DbgLog helpers that append to C:\Users\Public\weasel-compartment-debug.log (always-on for diagnosis). Thread-safe with CRITICAL_SECTION, with timestamps. Log OPENCLOSE/CONVERSION OnChange entry, decision branches, and _UpdateLanguageBar guard transitions.
|
发现这个PR在 windows 10 下没有问题,但 windows 11 下有问题,修复中... |
|
GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION 和 GUID_COMPARTMENT_KEYBOARD_OPENCLOSE 是不一样的消息,作用要区分。在GUID_COMPARTMENT_KEYBOARD_OPENCLOSE的情况下GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION的消息不应该再作任何响应,否则就是对旧功能的break,毕竟已知是有人要这种open/close状态切换而不是ascii切换(这也是传统小狼毫的基本状态)。需要明确的是,输入功能是主功能,外部消息控制是辅助,不能因为要引入辅助功能导致主功能失效是基本要求。 另外调试可以不用那么复杂,debugview++来实时看就行,调用weaselutility.h中 |
The CONVERSION handler called _SetKeyboardOpen(true) which writes OPENCLOSE=1 unconditionally. On Windows 11 this triggers the OPENCLOSE OnChange sink (else branch for _isToOpenClose=false), which blind- toggles ascii_mode, reversing the mode that CONVERSION just set. Debug logs confirmed the cascade: 1. CONVERSION sets ascii 0->1 (correct) 2. _SetKeyboardOpen(true) writes OPENCLOSE 3. OPENCLOSE handler toggles ascii 1->0 (wrong) 4. Final: ascii=0 (Chinese instead of English) Per Weasel maintainer guidance: OPENCLOSE and CONVERSION are separate compartments with separate responsibilities. OPENCLOSE manages enable/disable, CONVERSION manages Chinese/English. The CONVERSION handler should not touch OPENCLOSE. Callers (e.g. im-control/vim plugin) always send both -k open and -c together, so OPENCLOSE is handled by its own handler. When only CONVERSION is written (keyboard already open), OPENCLOSE handler doesn't fire and there is no cascade.
Record the ThreadMgr pointer and thread ID when WeaselTSF is activated, to compare with im-control's TF_GetThreadMgr pointer and determine if they share the same ThreadMgr instance in Windows Terminal.
The C:\Users\Public path may not be writable from all processes (e.g. UWP-packaged apps like Windows Terminal). Use GetTempPathA to write to the per-user temp directory. Also log PID in session header to distinguish multiple processes.
Remove _DbgInit/_DbgLog infrastructure, all debug log calls in Compartment.cpp, LanguageBar.cpp, and WeaselTSF.cpp. The production code is clean and contains only the functional fixes: - CONVERSION handler is value-driven (reads compartment, compares with state, only acts if different) - CONVERSION handler no longer calls _SetKeyboardOpen(true)
Add three-layer root cause analysis (TF_GetThreadMgr singleton, TfClientId, OPENCLOSE cascade), document the compartment decoupling principle, and add deployment note about restarting applications after DLL replacement.
Merge conversion-compartment-fix-analysis.md (blind toggle, Win10) and windows11-compatibility-fix.md (value-driven, Win11) into one coherent document: compartment-external-control-fix.md. The new document covers all four root causes, the complete fix across Weasel/im-control/vim-plugin, design principles, DLL deployment instructions (preserved from original), and verification results for both Windows 10 and 11.
感谢指导!我做了以下调整:
经过 windows 10 和 11 测试,目前均可以正常工作了。 |
|
更新总结之前的所有修订: WeaselTSF Compartment 外部控制修复分析问题通过 TSF compartment 程序化切换中英文模式(如 此问题在 Windows 10 和 Windows 11 上表现不同:
架构背景WeaselTSF 是 TSF Text Service(实现 两个 compartment 各司其职:
根因经 Windows 10 和 Windows 11 实测和日志分析,问题有四个独立的层面: 1. CONVERSION handler 撤销外部变更(Windows 10)
// 原始代码 (commit 93eec2d)
} else if (IsEqualGUID(guidCompartment,
GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) {
BOOL isOpen = _IsKeyboardOpen();
if (isOpen) {
weasel::ResponseParser parser(NULL, NULL, &_status, NULL,
&_cand->style());
bool ok = m_client.GetResponseData(std::ref(parser)); // 查询 RIME 后端当前状态
_UpdateLanguageBar(_status); // 写回 compartment
}
}问题链条:
原始代码从不读取 compartment 值来判断外部请求的目标模式,也不调用 2. CoCreateInstance(CLSID_TF_ThreadMgr) 在 Win11 返回新实例im-control 的 hook 用 日志证据:im-control 3. TF_CLIENTID_NULL 的 SetValue 在 Win11 不触发 OnChangeim-control 的 hook 用 Windows 10:不区分 4. CONVERSION handler 的 _SetKeyboardOpen(true) 引发级联反转CONVERSION handler 调用 日志证据: 移除 修复方案1. Weasel:CONVERSION handler 改为值驱动修改文件: 将原始的"查询后端 → 写回 compartment"改为"读取 compartment 值 → 比对状态 → 通知 RIME → 同步 UI": } else if (IsEqualGUID(guidCompartment,
GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) {
if (_updatingLanguageBar)
return S_OK;
DWORD convMode = 0;
_GetCompartmentDWORD(convMode,
GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION);
bool desiredAsciiMode = !(convMode & TF_CONVERSIONMODE_NATIVE);
if (desiredAsciiMode != _status.ascii_mode) {
_status.ascii_mode = desiredAsciiMode;
// 注意:不调用 _SetKeyboardOpen(true),避免触发 OPENCLOSE 级联
if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled())
_EnableLanguageBar(true);
_HandleLangBarMenuSelect(_status.ascii_mode
? ID_WEASELTRAY_ENABLE_ASCII
: ID_WEASELTRAY_DISABLE_ASCII);
if (_pEditSessionContext)
m_client.ClearComposition();
_UpdateLanguageBar(_status);
}
}天然幂等性:即使
2. Weasel:从 CONVERSION handler 移除 _SetKeyboardOpen(true)修改文件: CONVERSION handler 不再调用
3. Weasel:重入守卫修改文件:
// LanguageBar.cpp
void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) {
// ...
_updatingLanguageBar = true;
_SetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION);
_updatingLanguageBar = false;
// ...
}// WeaselTSF.h
BOOL _updatingLanguageBar = false;4. im-control:使用 TF_GetThreadMgr 获取 per-thread 单例修改文件: 用 msctf.dll 导出的 typedef HRESULT(WINAPI* PFN_TF_GetThreadMgr)(ITfThreadMgr**);
static ITfThreadMgr* GetThreadMgrSingleton() {
HMODULE hMsctf = GetModuleHandleW(L"msctf.dll");
// ... GetProcAddress("TF_GetThreadMgr") ...
ITfThreadMgr* pThreadMgr = nullptr;
pfn(&pThreadMgr);
return pThreadMgr;
}5. im-control:使用有效 TfClientId 调用 SetValue修改文件: 调用 TfClientId clientId = TF_CLIENTID_NULL;
pThreadMgr->Activate(&clientId);
// ... SetValue(clientId, ...) 替代 SetValue(0, ...) ...
pThreadMgr->Deactivate();6. im-control:OPENCLOSE 跳过未变化的写入修改文件: 写入前先 7. vim 插件:去掉 -k open,只写 CONVERSION修改文件:
设计原则遵循 fxliang 的指导:
我们的修改完全符合此原则:
方案对比
DLL 部署位置WeaselTSF 的 CLSID
TSF 框架从上述系统路径加载 DLL。如果将修复后的 DLL 部署到其他路径(如 更新方法管理员身份打开 PowerShell,执行: Rename-Item "C:\Windows\system32\weasel.dll" "weasel.dll.bak" -Force
Copy-Item "C:\path\to\weaselx64.dll" "C:\Windows\system32\weasel.dll" -Force
Rename-Item "C:\Windows\SysWOW64\weasel.dll" "weasel.dll.bak" -Force
Copy-Item "C:\path\to\weasel.dll" "C:\Windows\SysWOW64\weasel.dll" -Force重要:部署后需重启所有使用 RIME 的应用
编译Weasel在装有 Visual Studio 2022 + Boost 的机器上编译: 产出 im-controlCopy-Item bin\* "C:\Apps\VimReader\lib\utils\im-control\" -Forcevim 插件vim-im-select 插件为纯脚本,无需编译,pull 后 reload vim 配置即可。 验证测试环境
测试结果Windows 10 和 Windows 11 下均通过:
相关链接 |
Closes #1371