From 7e21123bad842e4ff024cf032f76977c2718d2ba Mon Sep 17 00:00:00 2001 From: aeoform <2790848120@qq.com> Date: Fri, 5 Jun 2026 21:59:12 +0800 Subject: [PATCH] fix(linux): fcitx5 hotkey intercept + remove backdrop-filter + clipboard cleanup - fcitx5 plugin: fix Alt+a hotkey leaking 'a' character by normalizing case comparison (Alt produces uppercase sym, Key::check expects lowercase) - fcitx5 plugin: use PreInputMethod + filterAndAccept to block engine before commit happens (same strategy as clipboard addon) - Linux: remove backdrop-filter (unreliable on WebKitGTK, #553), replace with opaque background - Linux: skip clipboard writes after streaming insert (fcitx5 commits directly, clipboard fallback corrupts user data) - SettingsModal: render via React Portal to document.body (#580, fixes scrollbars overlapping modal) - Remove is_no_compositing_mode command and CSS fallback (no longer needed) - Remove StartupShell loading screen (app renders FloatingShell immediately) Co-Authored-By: Claude Opus 4.7 (1M context) --- openless-all/app/src-tauri/src/commands.rs | 6 -- .../src-tauri/src/coordinator/dictation.rs | 5 +- openless-all/app/src-tauri/src/insertion.rs | 40 ++++++------ openless-all/app/src-tauri/src/lib.rs | 1 - openless-all/app/src/App.tsx | 13 ---- .../app/src/components/FloatingShell.tsx | 20 +++--- .../app/src/components/WindowChrome.tsx | 33 +++++----- openless-all/app/src/styles/global.css | 50 --------------- .../scripts/linux-fcitx5-plugin/openless.cpp | 62 ++++++------------- 9 files changed, 72 insertions(+), 158 deletions(-) diff --git a/openless-all/app/src-tauri/src/commands.rs b/openless-all/app/src-tauri/src/commands.rs index 79a8f5b4..1ed9af5e 100644 --- a/openless-all/app/src-tauri/src/commands.rs +++ b/openless-all/app/src-tauri/src/commands.rs @@ -3564,12 +3564,6 @@ pub async fn github_device_flow_poll( Ok(GithubDevicePollResult::Error { message: msg }) } -#[tauri::command] -pub fn is_no_compositing_mode() -> bool { - // Linux WebKitGTK: WEBKIT_DISABLE_COMPOSITING_MODE=1 时 backdrop-filter 不可用 - std::env::var("WEBKIT_DISABLE_COMPOSITING_MODE").as_deref() == Ok("1") -} - #[cfg(test)] mod tests { use super::{ diff --git a/openless-all/app/src-tauri/src/coordinator/dictation.rs b/openless-all/app/src-tauri/src/coordinator/dictation.rs index dfbbe3a5..4944a970 100644 --- a/openless-all/app/src-tauri/src/coordinator/dictation.rs +++ b/openless-all/app/src-tauri/src/coordinator/dictation.rs @@ -187,8 +187,9 @@ async fn run_streaming_polish( Some(e) => (typed_text, Some(format!("typing partially failed: {e}"))), None => (text, None), }; - // 把 final_text 写回剪贴板(默认 on,可关)。一次性路径天然走剪贴板, - // 开关默认对齐一次性行为,让 Cmd+V 重复粘贴可用。 + // 把 final_text 写回剪贴板(默认 on,macOS/Windows 适用)。 + // Linux:fcitx5 插件已直写文字到目标 app,跳过剪贴板避免破坏用户数据。 + #[cfg(not(target_os = "linux"))] if inner.prefs.get().streaming_insert_save_clipboard { match arboard::Clipboard::new() { Ok(mut cb) => match cb.set_text(final_text.clone()) { diff --git a/openless-all/app/src-tauri/src/insertion.rs b/openless-all/app/src-tauri/src/insertion.rs index 2d8c454b..d7876600 100644 --- a/openless-all/app/src-tauri/src/insertion.rs +++ b/openless-all/app/src-tauri/src/insertion.rs @@ -30,10 +30,30 @@ impl TextInserter { Self } - /// Windows/Linux 路径:写剪贴板 + 模拟 `paste_shortcut`。 + /// Linux 路径:仅走 fcitx5 CommitText 直写。无剪贴板 fallback。 + #[cfg(target_os = "linux")] + pub fn insert( + &self, + text: &str, + _restore_clipboard_after_paste: bool, + _paste_shortcut: PasteShortcut, + ) -> InsertStatus { + if text.is_empty() { + return InsertStatus::CopiedFallback; + } + match crate::linux_fcitx::commit_text(text) { + Ok(()) => InsertStatus::Inserted, + Err(e) => { + log::warn!("[insertion] fcitx commit_text failed: {e}"); + InsertStatus::Failed + } + } + } + + /// Windows 路径:写剪贴板 + 模拟 `paste_shortcut`。 /// - `restore_clipboard_after_paste`:粘贴后是否恢复用户原剪贴板。 /// - `paste_shortcut`:模拟按下的粘贴快捷键(如终端可能要 Ctrl+Shift+V)。 - #[cfg(not(target_os = "macos"))] + #[cfg(target_os = "windows")] pub fn insert( &self, text: &str, @@ -43,22 +63,6 @@ impl TextInserter { if text.is_empty() { return InsertStatus::CopiedFallback; } - // Linux: 始终优先使用 fcitx5 CommitText 直写(支持中文)。 - // 如果插件未加载,降级到剪贴板拷贝(统一路径,不单独维护 enigo XTest)。 - #[cfg(target_os = "linux")] - { - match crate::linux_fcitx::commit_text(text) { - Ok(()) => return InsertStatus::Inserted, - Err(e) => { - log::warn!("[insertion] fcitx commit_text failed: {e}, fallback to clipboard only"); - if copy_to_clipboard(text) { - return InsertStatus::CopiedFallback; - } - return InsertStatus::Failed; - } - } - } - #[cfg(not(target_os = "linux"))] insert_with_clipboard_restore(text, restore_clipboard_after_paste, paste_shortcut) } diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 4b65b3d8..3f1d8d19 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -471,7 +471,6 @@ pub fn run() { #[cfg(target_os = "windows")] commands::sherpa_onnx_asr_reveal_model_dir, commands::export_error_log, - commands::is_no_compositing_mode, restart_app, ]) .build(tauri::generate_context!()) diff --git a/openless-all/app/src/App.tsx b/openless-all/app/src/App.tsx index ff5dd5fc..b0a84116 100644 --- a/openless-all/app/src/App.tsx +++ b/openless-all/app/src/App.tsx @@ -17,7 +17,6 @@ import { windowMouseHotkeyCode, } from './lib/windowHotkeyFallback'; import { QaPanel } from './pages/QaPanel'; -import { invoke } from '@tauri-apps/api/core'; import { HotkeySettingsProvider } from './state/HotkeySettingsContext'; interface AppProps { @@ -169,18 +168,6 @@ export function App({ isCapsule, isQa, forcedOs }: AppProps) { }; }, [os]); - // Linux: 检测 WEBKIT_DISABLE_COMPOSITING_MODE → 触发磨砂 fallback(#570, #553) - useEffect(() => { - if (!isTauri) return; - invoke('is_no_compositing_mode').then((val) => { - if (val) { - document.documentElement.dataset.olNoCompositing = 'true'; - } - }).catch((err) => { - console.warn('[startup] is_no_compositing_mode failed', err); - }); - }, []); - return ( {gate === 'onboarding' ? setGate('ready')} /> : } diff --git a/openless-all/app/src/components/FloatingShell.tsx b/openless-all/app/src/components/FloatingShell.tsx index 96a99d09..e97cdf75 100644 --- a/openless-all/app/src/components/FloatingShell.tsx +++ b/openless-all/app/src/components/FloatingShell.tsx @@ -5,6 +5,7 @@ // Ported verbatim from design_handoff_openless/variants.jsx::FloatingShell. import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentType } from 'react'; +import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { Icon } from './Icon'; import { WindowChrome, detectOS, type OS } from './WindowChrome'; @@ -324,13 +325,13 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia 一起坐在 WindowChrome 的磨砂底板上,整体一块连续玻璃。 */}
- {/* Settings modal — rendered inside this window */} - {settingsOpen && + {/* Settings modal — Rendered to document.body via portal + to escape the main console's overflow:hidden stacking context, + preventing page scrollbars from rendering on top. */} + {settingsOpen && createPortal( setSettingsOpen(false)} - /> - } + />, + document.body, + )} {providerPromptOpen ? ( {os === 'mac' && ( @@ -152,7 +154,6 @@ function LinuxTitlebar() { return (
watchEvent( EventType::InputContextKeyEvent, EventWatcherPhase::PreInputMethod, [this](Event &event) { auto &keyEvent = static_cast(event); - // 保存当前输入上下文:快捷键按下时用户在目标 app 中, - // 此后胶囊窗口可能抢走焦点,但 commitText 仍能用此 IC 提交文字。 if (!keyEvent.isRelease()) { savedIc_ = keyEvent.inputContext(); } - auto sym = static_cast(keyEvent.key().sym()); auto states = static_cast(keyEvent.key().states()); bool isPress = !keyEvent.isRelease(); - // 检查自定义组合键(优先级最高) - if (hasCustomDictationKey_ && - keyEvent.key().check(customDictationKey_)) { + // 自定义组合键:Alt 状态下字母 sym 可能大写(A vs a),归一化比较 + if (hasCustomDictationKey_ && states == static_cast(customDictationKey_.states()) && + (sym == static_cast(customDictationKey_.sym()) || + (sym >= 65 && sym <= 90 && sym + 32 == static_cast(customDictationKey_.sym())) || + (sym >= 97 && sym <= 122 && sym - 32 == static_cast(customDictationKey_.sym())))) { FCITX_LOGC(openless, Debug) - << "Custom dictation combo: sym=" << sym - << " states=" << states - << " isPress=" << isPress; + << "Custom dictation: sym=" << sym << " states=" << states; dictationKeyEvent( static_cast(customDictationKey_.sym()), static_cast(customDictationKey_.states()), @@ -127,8 +126,6 @@ public: keyEvent.filterAndAccept(); return; } - - // 检查听写触发键(raw + keylist 双路径) if ((triggerRawSym_ != 0 && keyEvent.key().check(Key(static_cast(triggerRawSym_), static_cast(triggerRawStates_)))) || @@ -140,55 +137,35 @@ public: } return false; }())) { - auto dsym = triggerRawSym_ != 0 - ? triggerRawSym_ + auto dsym = triggerRawSym_ != 0 ? triggerRawSym_ : static_cast(triggerKeyList_[0].sym()); - auto dstates = triggerRawStates_ != 0 - ? triggerRawStates_ + auto dstates = triggerRawStates_ != 0 ? triggerRawStates_ : static_cast(triggerKeyList_[0].states()); FCITX_LOGC(openless, Debug) - << "Dictation hotkey: sym=" << dsym - << " states=" << dstates - << " isPress=" << isPress; + << "Dictation hotkey sym=" << dsym; dictationKeyEvent(dsym, dstates, isPress); keyEvent.filterAndAccept(); return; } - - // 检查 QA 快捷键 - if (qaRawSym_ != 0 && - sym == qaRawSym_ && + if (qaRawSym_ != 0 && sym == qaRawSym_ && states == qaRawStates_) { FCITX_LOGC(openless, Debug) - << "QA shortcut: sym=" << qaRawSym_ - << " states=" << qaRawStates_ - << " isPress=" << isPress; + << "QA shortcut"; qaShortcutEvent(qaRawSym_, qaRawStates_, isPress); keyEvent.filterAndAccept(); return; } - - // 检查翻译模式修饰键(自定义 + 内置 Shift) bool translationMatched = false; - if (translationRawSym_ != 0 && - sym == translationRawSym_ && - states == translationRawStates_) { + if (translationRawSym_ != 0 && sym == translationRawSym_ && + states == translationRawStates_) translationMatched = true; - } - // 内置 Shift 修饰键(仅在配置了自定义翻译键时生效, - // 避免无翻译配置时每次按 Shift 都触发信号)。 if (translationRawSym_ != 0 && - (sym == 0xffe1 || sym == 0xffe2)) { + (sym == 0xffe1 || sym == 0xffe2)) translationMatched = true; - } if (translationMatched) { FCITX_LOGC(openless, Debug) - << "Translation modifier: sym=" << sym - << " states=" << states - << " isPress=" << isPress; + << "Translation modifier: sym=" << sym; translationModifierEvent(sym, states, isPress); - // 不 filterAndAccept:修改键只需通知 OpenLess 翻译状态变更, - // 不应阻塞输入法引擎处理 Shift 事件(如中英文切换)。 } })); @@ -222,8 +199,7 @@ public: instance_->flushUI(); })); - // 6. PostInputMethod 阶段恢复 lastAuxText_:fcitx5 在处理按键后可能 - // 清掉 auxDown(如 enter/backspace 触发内联模式),此钩子自动补回。 + // 6. PostInputMethod:恢复 auxDown(fcitx5 内联模式/方向键后可能清掉) eventHandlers_.push_back( instance_->watchEvent( EventType::InputContextKeyEvent,