fix(commands): wrap tray refresh in run_on_main_thread to avoid macOS UI freeze (#380)
set_settings 和 set_default_polish_mode 是同步 Tauri commands,跑在 IPC handler 线程。它们直接调 refresh_tray_microphone_menu,里面 tray.set_menu(...) 改 macOS NSStatusItem 必须在主线程上做。从 IPC 线程直调会触发 macOS dispatch queue 死锁,导致用户改任何偏好开关后整个 UI 永久卡死、所有按键无响应。 修复:把两处 tray 刷新都包到 app.run_on_main_thread(move || ...) dispatch 到主线程, IPC 线程立即返回不阻塞。这跟 lib.rs:558 里 start_tray_microphone_watcher 已经 在用的 pattern 完全一致(issue #169 stop_qa_hotkey_listener 同样用法)。 防御性扫描结论:整个项目里 tray.set_menu() 只在 refresh_tray_microphone_menu (lib.rs:518)一处出现,所有 5 个调用点已确认安全: - lib.rs:207 tray hover event:tray 事件本身就在主线程 - lib.rs:559 麦克风设备 watcher:已有 run_on_main_thread - lib.rs:605 tray 菜单事件:本身在主线程 - commands.rs:160 set_settings:本提交修复 - commands.rs:944 set_default_polish_mode:本提交修复 其它 sync IPC commands(set_dictation/qa/translation/switch_style/open_app_hotkey) 通过 coord.refresh_*_hotkey() 间接刷新,那些函数内部已包了 run_on_main_thread; 不碰 AppKit 的 commands 安全。 Co-authored-by: baiqing <lbx12309@icloud.com>
This commit is contained in:
1 parent
feadbfa70b
commit
14bfa5f749
1 file changed
+24
-7
@@ -150,10 +150,22 @@ pub fn set_settings(
|
||||
// 没有 HotkeySettingsContext,必须靠事件感知录音键变化,否则面板可见时
|
||||
// 用户改键会让浮窗里的 "{recordHotkey}" 文案一直停留在旧值。
|
||||
persist_settings(&*coord, prefs.clone())?;
|
||||
if let Err(err) = crate::refresh_tray_microphone_menu(&app) {
|
||||
log::warn!("[tray] refresh microphone menu after settings save failed: {err}");
|
||||
sync_tray_microphone_selection(&tray_microphones.lock(), &prefs.microphone_device_name);
|
||||
}
|
||||
// refresh_tray_microphone_menu 内部会调用 NSStatusItem.set_menu,必须在主线程上跑。
|
||||
// set_settings 本身是同步 Tauri command,在 IPC handler 线程上执行;从这里直接调
|
||||
// 会触发 macOS 主线程断言或在 dispatch 队列上死锁,导致整个 UI 无响应(用户改
|
||||
// 偏好后所有按键都没反应即此根因)。dispatch 到主线程后立即返回,IPC 线程不阻塞。
|
||||
let app_for_main = app.clone();
|
||||
let prefs_for_main = prefs.clone();
|
||||
let _ = app.run_on_main_thread(move || {
|
||||
if let Err(err) = crate::refresh_tray_microphone_menu(&app_for_main) {
|
||||
log::warn!("[tray] refresh microphone menu after settings save failed: {err}");
|
||||
let tray_state = app_for_main.state::<TrayMicrophoneMenuState>();
|
||||
sync_tray_microphone_selection(&tray_state.lock(), &prefs_for_main.microphone_device_name);
|
||||
}
|
||||
});
|
||||
// 抑制 unused 警告:tray_microphones 现在改在闭包里通过 app.state 取,
|
||||
// 但函数签名保留 State 入参,以便 Tauri 在调用前注入。
|
||||
let _ = tray_microphones;
|
||||
let _ = app.emit("prefs:changed", &prefs);
|
||||
Ok(())
|
||||
}
|
||||
@@ -929,9 +941,14 @@ pub fn set_default_polish_mode(
|
||||
let mut prefs = coord.prefs().get();
|
||||
prefs.default_mode = mode;
|
||||
coord.prefs().set(prefs.clone()).map_err(|e| e.to_string())?;
|
||||
if let Err(err) = crate::refresh_tray_microphone_menu(&app) {
|
||||
log::warn!("[tray] refresh style menu after polish mode IPC change failed: {err}");
|
||||
}
|
||||
// 跟 set_settings 同样:refresh_tray_microphone_menu 里 tray.set_menu 改 NSStatusItem,
|
||||
// 必须主线程;这里是同步 Tauri command 跑在 IPC 线程,直调会让 macOS 死锁。
|
||||
let app_for_main = app.clone();
|
||||
let _ = app.run_on_main_thread(move || {
|
||||
if let Err(err) = crate::refresh_tray_microphone_menu(&app_for_main) {
|
||||
log::warn!("[tray] refresh style menu after polish mode IPC change failed: {err}");
|
||||
}
|
||||
});
|
||||
let _ = app.emit("prefs:changed", &prefs);
|
||||
let _ = app.emit_to("main", "prefs:changed", &prefs);
|
||||
Ok(())
|
||||
|
||||
Reference in new issue
Block a user