* fix(windows): 修复 #466 #468 #469 IME 切回 / 静默启动 / QA 焦点 + #470 capsule 诊断 (#471)
集中修复 4 个 Windows-only bug:
- #469 IME 切不回 — restore_profile 对称补 legacy TSF ChangeCurrentLanguage + ActivateLanguageProfile,回归点是 c3b59329 升级激活路径时漏改恢复路径。现代 ActivateProfile 失败降为 warn(legacy 已完成视觉切回)。
- #468 Win11 静默启动失效 — single-instance 回调读 start_minimized,true 时跳过 show_main_window;托盘 / CLI intent 路径不受影响。
- #466 QA 弹窗 X/ESC 关不掉 — Win 上从 SW_SHOWNOACTIVATE 切到 Tauri window.show + set_focus,让 webview 拿键盘焦点。配套 focus-dance:begin_qa_session 抓选区前临时把焦点还给用户原 app,抓完再 refocus QA;多轮场景下用 GetWindowThreadProcessId 区分本进程窗口,刷新 qa_focus_target,避免 stale HWND。
- #470 录音胶囊不显示 — 加 per-process 一次性诊断 log(CapsuleState 用 capsule_state_log_name 显式短名,不走 Debug 防字段扩展时泄露),下次能直接定位 prefs / state / Win32 哪一环失败。
pr_agent 五轮迭代到 No major issues + No security concerns;Cloud 评审建议的 begin_qa_session 双锁 TOCTOU 已合并为单 scope。三平台 build + pr_agent CI 全绿。
#470 没标 closes,是 refs—需要真机日志才能定位真因,先 ship 诊断手段。
* chore(release): bump version to 1.3.4-3 (Beta) — Windows #466 #468 #469 修复
* fix(settings-ui): 权限页徽章不换行 + 本地 ASR 区块 Win 灰显
权限页 (Settings → 快捷键 → 权限):
- Pill 组件加 whiteSpace:nowrap + flexShrink:0 → "已安装"徽章在 Win
窄宽下不再被挤成 3 行(每个汉字一行)。
- 行内 message span 加 whiteSpace:nowrap + overflow:hidden +
textOverflow:ellipsis + minWidth:0 → 长文本("Windows 低层键盘 hook
已安装")超宽时省略号收尾,徽章保持完整。
- zh-CN 描述大段精简:descAcc / descNoAcc / micDesc / accDesc /
hotkeyDesc / networkDesc / windowsImeDesc / windowsIme.* 都改短,
减少 Win 上拥挤感(用户反馈 zh 文案过长)。
高级页 (Settings → 高级 → 本地 ASR):
- Windows 上把"本地 ASR 模型(实验性)"标题区 + 警告小字 + Qwen3 行
整组 opacity:0.45 灰显 —— Qwen3 在 Win 是 stub 不支持,那条"实验性"
主线在 Win 没意义;用户视觉关注点应落到下方独立的 Foundry 行(Foundry
保持正常颜色)。Toggle 的 disabled 行为已经在原 onToggle 条件里,本次
纯加视觉灰显。
* fix(windows): 前端 App.tsx 在 show 主窗口前读 startMinimized 兜底
#471 在 Rust 端 setup() / single-instance callback 都加了 start_minimized
suppression,本机日志确认 Rust 路径完全正确("[main] start_minimized=true
→ 跳过初始 show")。但用户在 Win11 1.3.4-3 上仍然「重启进桌面那一刻主
窗口就出来了」。
定位到最后一条遗漏路径在前端:App.tsx 里 mount 时一条 useEffect 无条件
通过 IPC 调 `getCurrentWindow().show()`,把 Rust 端已经 suppress 的窗口
又拉出来 —— 这条路径 Rust log 完全看不见(走的是 plugin-window 的
showWindow IPC,不经过我们的 show_main_window helper)。
那条 useEffect 当初是给 issue #163 引入的:Windows 权限探测可能死锁让
首屏卡灰,所以前端兜底 force show 一次。
修法:show 前先读 prefs.startMinimized,true 就 return。读 prefs 失败
(极少数情况)保持原 show 行为,不让 #163 的兜底语义丢失。
closes #468 (the actual one this time, Rust + JS 双端都收敛了)
---------
Co-authored-by: baiqing <lbx12309@icloud.com>
This commit is contained in:
1 parent
ceda87f64f
commit
b4e806e3ca
10 files changed
+217
-36
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openless-app",
|
||||
"version": "1.3.4-2",
|
||||
"version": "1.3.4-3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openless-app",
|
||||
"version": "1.3.4-2",
|
||||
"version": "1.3.4-3",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.1.1",
|
||||
"@tauri-apps/plugin-autostart": "^2.5.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "openless-app",
|
||||
"private": true,
|
||||
"version": "1.3.4-2",
|
||||
"version": "1.3.4-3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Generated
+1
-1
@@ -3666,7 +3666,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openless"
|
||||
version = "1.3.4-2"
|
||||
version = "1.3.4-3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "openless"
|
||||
version = "1.3.4-2"
|
||||
version = "1.3.4-3"
|
||||
description = "OpenLess — local voice input that types where your cursor is"
|
||||
authors = ["OpenLess"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -94,6 +94,23 @@ fn capsule_show_strategy_for_platform() -> CapsuleShowStrategy {
|
||||
}
|
||||
|
||||
static CAPSULE_NO_ACTIVATE_FALLBACK_WARNED: AtomicBool = AtomicBool::new(false);
|
||||
static CAPSULE_SUPPRESSED_BY_TOGGLE_LOGGED: AtomicBool = AtomicBool::new(false);
|
||||
static CAPSULE_FIRST_SHOW_LOGGED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// 给 #470 诊断日志用的 capsule 状态短名。显式枚举每个变体到 &'static str,
|
||||
/// 不走 `Debug` —— 哪天 CapsuleState 加了 `String` 字段,`:?` 会把 ASR / polish
|
||||
/// 内容意外灌进日志(pr_agent 提的 forward-looking 隐患);这里只输出状态名。
|
||||
fn capsule_state_log_name(state: CapsuleState) -> &'static str {
|
||||
match state {
|
||||
CapsuleState::Idle => "idle",
|
||||
CapsuleState::Recording => "recording",
|
||||
CapsuleState::Transcribing => "transcribing",
|
||||
CapsuleState::Polishing => "polishing",
|
||||
CapsuleState::Done => "done",
|
||||
CapsuleState::Cancelled => "cancelled",
|
||||
CapsuleState::Error => "error",
|
||||
}
|
||||
}
|
||||
|
||||
fn show_capsule_window_for_recording<R: tauri::Runtime>(
|
||||
app: &AppHandle<R>,
|
||||
@@ -2617,8 +2634,39 @@ async fn begin_qa_session(inner: &Arc<Inner>) -> Result<(), String> {
|
||||
inner.qa_stream_cancelled.store(false, Ordering::SeqCst);
|
||||
|
||||
// 抓选区。每轮按 Option 都重新抓一次:用户多轮提问中可以重新选别处文字。
|
||||
// 浮窗 focus:false,原 app 仍是 frontmost,AX/Cmd+C fallback 都能拿到。
|
||||
//
|
||||
// - macOS:浮窗走 orderFrontRegardless,不成为 key window,原 app 仍是 frontmost,
|
||||
// AX/Cmd+C fallback 都能拿到。
|
||||
// - Windows:#466 修复后 show_qa_window_no_activate 主动抓焦点,QA 此刻已是前台,
|
||||
// simulate_copy 会跑在 QA 自己 webview 上 → 抓不到。focus-dance 上半场:把焦点临时
|
||||
// 还给"用户原 app 的 HWND"。
|
||||
//
|
||||
// 多轮场景的目标刷新:用户开 QA 后可能 Alt+Tab 切到别的 app 选新文字。如果还死认
|
||||
// open_qa_panel 时记下的初始 HWND,会把焦点抢回错的 app(pr_agent stale-focus 关注点)。
|
||||
// 策略:每轮先看当前前台是不是本进程的窗口(QA / capsule / main)—— 是 → 用户没切
|
||||
// 走,沿用 saved;不是 → 用户切到了真正的外部 app,刷新 saved 为当前 HWND。
|
||||
// 抓完选区后下半场再把焦点交还 QA,让 ESC/X 继续可用。
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// 合并两次 lock:原来分 lock #1 写 + lock #2 读,两者之间 close_qa_panel 在别的
|
||||
// 线程把 qa_focus_target 清成 None 会被覆盖回旧 HWND。Cloud 评审指出的 TOCTOU。
|
||||
// 单次加锁里既写最新外部前台、再读出来交给后面的 restore_focus_target_if_possible
|
||||
// —— capture_external_focus_target() 内部只调 GetForegroundWindow / pid 查询,
|
||||
// 不会反向取 qa_state 锁,持锁期间调用安全。
|
||||
let saved_target = {
|
||||
let mut state = inner.qa_state.lock();
|
||||
if let Some(current_external) = capture_external_focus_target() {
|
||||
state.qa_focus_target = Some(current_external);
|
||||
}
|
||||
state.qa_focus_target
|
||||
};
|
||||
let _ = restore_focus_target_if_possible(saved_target);
|
||||
}
|
||||
let selection = capture_selection();
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(app) = inner.app.lock().clone() {
|
||||
crate::refocus_qa_window(&app);
|
||||
}
|
||||
let selection_preview_text = selection.as_ref().map(|s| s.text.clone());
|
||||
inner.qa_state.lock().selection = selection.clone();
|
||||
|
||||
@@ -3929,6 +3977,33 @@ fn schedule_capsule_idle(inner: &Arc<Inner>, delay_ms: u64) {
|
||||
});
|
||||
}
|
||||
|
||||
/// 与 capture_focus_target 类似,但前台窗口属于本进程(即用户停在 QA / capsule / main
|
||||
/// 等自家窗口)时返回 None,让 caller 区分"用户没切到别处" vs "用户切到了另一个真正的
|
||||
/// 外部 app"。issue #466 多轮场景下用来刷新 qa_focus_target。
|
||||
#[cfg(target_os = "windows")]
|
||||
fn capture_external_focus_target() -> Option<usize> {
|
||||
use windows::Win32::System::Threading::GetCurrentProcessId;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{GetForegroundWindow, GetWindowThreadProcessId};
|
||||
|
||||
unsafe {
|
||||
let hwnd = GetForegroundWindow();
|
||||
if hwnd.0.is_null() {
|
||||
return None;
|
||||
}
|
||||
let mut pid: u32 = 0;
|
||||
GetWindowThreadProcessId(hwnd, Some(&mut pid));
|
||||
if pid == GetCurrentProcessId() {
|
||||
return None;
|
||||
}
|
||||
Some(hwnd.0 as usize)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn capture_external_focus_target() -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn capture_focus_target() -> Option<usize> {
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
|
||||
@@ -4304,12 +4379,33 @@ fn emit_capsule(
|
||||
// 处理,不依赖把 Done/Cancelled/Error 打成 invisible。详见 PR #140 评论。
|
||||
maybe_position_capsule_bottom_center(&inner_for_main, &window, translation);
|
||||
if show_capsule && visible {
|
||||
// 用户报"看不到胶囊"时第一时间能在 log 里确认:胶囊路径有跑、show_capsule
|
||||
// 开关是 true、当前进入 visible 帧 —— 排除 prefs 没存住 / emit_capsule 没触
|
||||
// 发 / state 一直 Idle 这几类常见 root cause。issue #470。
|
||||
if !CAPSULE_FIRST_SHOW_LOGGED.swap(true, Ordering::SeqCst) {
|
||||
log::info!(
|
||||
"[capsule] first show this session: show_capsule=true visible=true state={}",
|
||||
capsule_state_log_name(state)
|
||||
);
|
||||
}
|
||||
show_capsule_window_for_recording(&app_for_main, &window);
|
||||
// macOS/Windows 优先走 no-activate show,避免录音胶囊抢走当前工作 app 焦点。
|
||||
// 若 fallback 到 show(),OpenLess 已是前台 app 时再把 key window 还给 main。
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::restore_main_window_key_if_active(&app_for_main);
|
||||
} else {
|
||||
// show_capsule 开关被用户关掉但本次确实想显示(visible=true)的情况:
|
||||
// 一次性 info log,让用户报"胶囊没显示"时能在日志里一眼看到根因 —— 维护者
|
||||
// 不必再让用户"去打开设置确认"。issue #470。
|
||||
if !show_capsule
|
||||
&& visible
|
||||
&& !CAPSULE_SUPPRESSED_BY_TOGGLE_LOGGED.swap(true, Ordering::SeqCst)
|
||||
{
|
||||
log::info!(
|
||||
"[capsule] suppressed by user toggle: show_capsule=false visible=true state={}",
|
||||
capsule_state_log_name(state)
|
||||
);
|
||||
}
|
||||
hide_capsule_window_if_present();
|
||||
let _ = window.hide();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ use crate::selection::SelectionContext;
|
||||
use crate::types::CapsuleState;
|
||||
|
||||
use super::{
|
||||
begin_qa_session, cancel_qa_session, capture_frontmost_app, emit_capsule, end_qa_session, Inner,
|
||||
begin_qa_session, cancel_qa_session, capture_focus_target, capture_frontmost_app, emit_capsule,
|
||||
end_qa_session, Inner,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -22,6 +23,10 @@ pub(super) struct QaSessionState {
|
||||
pub(super) cancelled: bool,
|
||||
pub(super) selection: Option<SelectionContext>,
|
||||
pub(super) front_app: Option<String>,
|
||||
/// open_qa_panel 时用户原 app 的 HWND(Windows 专用,存 usize 跨线程安全)。
|
||||
/// begin_qa_session 抓选区前临时把焦点还给它,避开 #466 修复后 QA 自己抢前台导致
|
||||
/// simulate_copy 在 QA webview 上跑空。非 Windows / macOS 平台为 None 不参与。
|
||||
pub(super) qa_focus_target: Option<usize>,
|
||||
/// 用于忽略迟到的 RMS / runtime error。
|
||||
pub(super) session_id: SessionId,
|
||||
/// QA 浮窗是否被用户钉住(pinned)。pinned=true 时不自动隐藏。
|
||||
@@ -41,6 +46,7 @@ impl Default for QaSessionState {
|
||||
cancelled: false,
|
||||
selection: None,
|
||||
front_app: None,
|
||||
qa_focus_target: None,
|
||||
session_id: initial_session_id(),
|
||||
pinned: false,
|
||||
panel_visible: false,
|
||||
@@ -85,6 +91,9 @@ pub(super) fn open_qa_panel(inner: &Arc<Inner>) {
|
||||
state.messages.clear();
|
||||
state.selection = None;
|
||||
state.front_app = capture_frontmost_app();
|
||||
// 在 show_qa_window 抢前台之前抓一下:每次 begin_qa_session 抓选区时拿这个 HWND
|
||||
// 临时把焦点还回去,让 simulate_copy 跑在用户原 app 上。issue #466 focus-dance。
|
||||
state.qa_focus_target = capture_focus_target();
|
||||
}
|
||||
// 主听写 phase 是 Idle 才需要 sweep capsule —— 这里的语义是清掉「上一次 dictation
|
||||
// Done 状态残留」的 message / insertedChars,让 QA 自己的 capsule 状态从干净起跑
|
||||
@@ -119,6 +128,7 @@ pub(super) fn close_qa_panel(inner: &Arc<Inner>) {
|
||||
state.messages.clear();
|
||||
state.selection = None;
|
||||
state.front_app = None;
|
||||
state.qa_focus_target = None;
|
||||
state.phase = QaPhase::Idle;
|
||||
state.cancelled = false;
|
||||
}
|
||||
|
||||
@@ -85,6 +85,20 @@ pub fn run() {
|
||||
dispatch_cli_intent(app, intent);
|
||||
return;
|
||||
}
|
||||
// 静默启动模式下:第二次启动(Win11 的「登录时重新打开应用」、autostart 双触发、
|
||||
// 或用户手动再点图标)也不弹主窗口,否则 start_minimized=true 在 Win11 上整体失效。
|
||||
// 用户想看主窗口走托盘菜单 / 托盘左键。issue #468。
|
||||
if let Some(coordinator) = app
|
||||
.try_state::<Arc<coordinator::Coordinator>>()
|
||||
.map(|s| Arc::clone(&*s))
|
||||
{
|
||||
if coordinator.prefs().get().start_minimized {
|
||||
log::info!(
|
||||
"[single-instance] start_minimized=true → skipping show on relaunch"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
log::info!(
|
||||
"[single-instance] another instance launched, focusing existing main window"
|
||||
);
|
||||
@@ -1117,24 +1131,38 @@ pub(crate) fn hide_qa_window<R: tauri::Runtime>(app: &AppHandle<R>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 抓完选区后把焦点重新交回 QA 浮窗(Windows focus-dance 下半场)。begin_qa_session
|
||||
/// 在 capture_selection 跑完时调;非 Windows 平台是 no-op。issue #466。
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn refocus_qa_window<R: tauri::Runtime>(app: &AppHandle<R>) {
|
||||
if let Some(window) = app.get_webview_window("qa") {
|
||||
let _ = show_qa_window_no_activate(&window);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn refocus_qa_window<R: tauri::Runtime>(_app: &AppHandle<R>) {}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn show_qa_window_no_activate<R: tauri::Runtime>(window: &tauri::WebviewWindow<R>) -> bool {
|
||||
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_SHOWNOACTIVATE};
|
||||
|
||||
let Ok(handle) = window.window_handle() else {
|
||||
return false;
|
||||
};
|
||||
let RawWindowHandle::Win32(raw) = handle.as_raw() else {
|
||||
return false;
|
||||
};
|
||||
let hwnd = HWND(raw.hwnd.get() as *mut _);
|
||||
if hwnd.0.is_null() {
|
||||
// 函数名沿用历史命名,实际行为已切到「show + focus」—— 让 QA webview 真正拿到键盘
|
||||
// 焦点,ESC 才能到 React 监听、X 按钮 first-click 才不会被 OS 当作激活点击吃掉。
|
||||
//
|
||||
// 走 Tauri 的 show() / set_focus() 而不是 Win32 SetForegroundWindow + SetFocus
|
||||
// 的原因(pr_agent 关注点二轮回应):
|
||||
// - 直接 SetFocus(host_hwnd) 不保证 WebView2 child 收键盘事件,WebView2 子窗口
|
||||
// 有自己的 focus 模型。Tauri 内部走 webview 专用路径,能把焦点真正送到 webview。
|
||||
// - SetForegroundWindow 在 Win11 focus-stealing prevention 下可能被拒。Tauri
|
||||
// 2.x 在跨平台 abstraction 里做了兜底(按 SPI 临时调整 / attach input queue)。
|
||||
//
|
||||
// 对 issue #164 "QA 浮窗不抢前台 app 焦点"的取舍:浮窗出现时会短暂成为前台,
|
||||
// 但 begin_qa_session 抓选区前 focus-dance 会把焦点临时还给用户原 app(见
|
||||
// coordinator.rs 同 issue 注释),抓完再 refocus_qa_window 收回 —— 选区路径
|
||||
// 仍能正常工作,issue #164 在「QA 出现的那一帧」短暂被违背是 #466 修复的代价。
|
||||
if window.show().is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let _ = unsafe { ShowWindow(hwnd, SW_SHOWNOACTIVATE) };
|
||||
let _ = window.set_focus();
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -319,37 +319,71 @@ mod windows_impl {
|
||||
}
|
||||
|
||||
pub fn restore_profile(snapshot: &ImeProfileSnapshot) -> WindowsImeProfileResult<()> {
|
||||
// 必须与 activate_openless_profile 路径对称:激活同时调了 legacy
|
||||
// ITfInputProcessorProfiles 的 ChangeCurrentLanguage + ActivateLanguageProfile,
|
||||
// 单独调现代 ITfInputProcessorProfileMgr::ActivateProfile 不会更新 legacy
|
||||
// current language / active profile 状态,OS 仍认 OpenLess 是当前输入法 →
|
||||
// 用户的输入法切不回去。issue #469。
|
||||
//
|
||||
// 现代 ActivateProfile 失败降级为 warn:legacy 两步成功后,OS 视觉层已经把用户
|
||||
// 原 IME 切回(语言指示器、键盘事件路由都走 legacy 视图);现代 API 失败只是内部
|
||||
// bookkeeping 不同步,不会让用户看到"还停在 OpenLess"。所以这一步降级为 warn,
|
||||
// 不让 caller 把"已经切回了但 bookkeeping 慢"误判成"切回完全失败"。pr_agent
|
||||
// partial-restore 关注点回应。
|
||||
match snapshot.kind() {
|
||||
ImeProfileKind::TextService => {
|
||||
let clsid = parse_required_guid("text service CLSID", snapshot.clsid())?;
|
||||
let profile_guid =
|
||||
parse_required_guid("text service profile GUID", snapshot.profile_guid())?;
|
||||
let lang_id = snapshot.lang_id();
|
||||
|
||||
with_profile_manager(|manager| unsafe {
|
||||
with_input_processor_profiles(|profiles| unsafe {
|
||||
profiles.ChangeCurrentLanguage(lang_id)?;
|
||||
profiles.ActivateLanguageProfile(&clsid, lang_id, &profile_guid)
|
||||
})?;
|
||||
|
||||
let modern_result = with_profile_manager(|manager| unsafe {
|
||||
manager.ActivateProfile(
|
||||
TF_PROFILETYPE_INPUTPROCESSOR,
|
||||
snapshot.lang_id(),
|
||||
lang_id,
|
||||
&clsid,
|
||||
&profile_guid,
|
||||
null_hkl(),
|
||||
PROFILE_RESTORE_FLAGS,
|
||||
)
|
||||
})
|
||||
});
|
||||
if let Err(err) = modern_result {
|
||||
log::warn!(
|
||||
"[windows-ime] legacy restore OK but modern ActivateProfile failed: {err}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
ImeProfileKind::KeyboardLayout => {
|
||||
let hkl = HKL(snapshot.hkl().unwrap_or_default() as *mut c_void);
|
||||
let zero_guid = GUID::zeroed();
|
||||
let lang_id = snapshot.lang_id();
|
||||
|
||||
with_profile_manager(|manager| unsafe {
|
||||
with_input_processor_profiles(|profiles| unsafe {
|
||||
profiles.ChangeCurrentLanguage(lang_id)
|
||||
})?;
|
||||
|
||||
let modern_result = with_profile_manager(|manager| unsafe {
|
||||
manager.ActivateProfile(
|
||||
TF_PROFILETYPE_KEYBOARDLAYOUT,
|
||||
snapshot.lang_id(),
|
||||
lang_id,
|
||||
&zero_guid,
|
||||
&zero_guid,
|
||||
hkl,
|
||||
PROFILE_RESTORE_FLAGS,
|
||||
)
|
||||
})
|
||||
});
|
||||
if let Err(err) = modern_result {
|
||||
log::warn!(
|
||||
"[windows-ime] legacy restore OK but modern ActivateProfile (keyboard) failed: {err}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "OpenLess",
|
||||
"version": "1.3.4-2",
|
||||
"version": "1.3.4-3",
|
||||
"identifier": "com.openless.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
checkAccessibilityPermission,
|
||||
checkMicrophonePermission,
|
||||
getHotkeyStatus,
|
||||
getSettings,
|
||||
handleWindowHotkeyEvent,
|
||||
isTauri,
|
||||
} from './lib/ipc';
|
||||
@@ -43,14 +44,26 @@ export function App({ isCapsule, isQa }: AppProps) {
|
||||
let cancelled = false;
|
||||
requestAnimationFrame(() => {
|
||||
if (cancelled) return;
|
||||
import('@tauri-apps/api/window')
|
||||
.then(async ({ getCurrentWindow }) => {
|
||||
const currentWindow = getCurrentWindow();
|
||||
if (!(await currentWindow.isVisible())) {
|
||||
await currentWindow.show();
|
||||
}
|
||||
})
|
||||
.catch(error => console.warn('[startup] show main window failed', error));
|
||||
(async () => {
|
||||
// 尊重 prefs.startMinimized:开了静默启动就别在前端强 show 主窗口。否则
|
||||
// Rust 端 setup() 抑制掉的窗口,会被这条 useEffect 在 webview 加载完成后
|
||||
// 再通过 IPC 拉出来 —— issue #468 在 Rust 修复后用户仍能在 Win11 上复现
|
||||
// 的最后一条路径(Rust log 里看不到,因为走的是 plugin-window 的 IPC)。
|
||||
try {
|
||||
const prefs = await getSettings();
|
||||
if (prefs.startMinimized) return;
|
||||
} catch (err) {
|
||||
// 读 prefs 失败兜底走原有 show 行为:让权限探测失败的用户也能进 UI,
|
||||
// 避免透明 / 空白窗口前卡死(issue #163 引入这个 show 的原始意图)。
|
||||
console.warn('[startup] read startMinimized failed, falling back to show', err);
|
||||
}
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
if (cancelled) return;
|
||||
const currentWindow = getCurrentWindow();
|
||||
if (!(await currentWindow.isVisible())) {
|
||||
await currentWindow.show();
|
||||
}
|
||||
})().catch(error => console.warn('[startup] show main window failed', error));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
Reference in new issue
Block a user