diff --git a/src/main/api/renderer/settings.ts b/src/main/api/renderer/settings.ts index 8d518253..b74a30cd 100644 --- a/src/main/api/renderer/settings.ts +++ b/src/main/api/renderer/settings.ts @@ -53,6 +53,8 @@ export class SettingsAPI { // 全局快捷键配置映射(存储每个快捷键的 autoCopy 等配置) private globalShortcutConfigs: Map = new Map() private globalShortcutKeyboardStateReleasers = new Map void>() + // 全局快捷键触发流程执行中时,后续触发会被忽略,避免重复复制和重复启动。 + private isGlobalShortcutTriggering = false private setupIPC(): void { // 主题 @@ -379,41 +381,59 @@ export class SettingsAPI { preparation: GlobalShortcutPreparation ): Promise { if (!this.shouldTriggerGlobalShortcut(preparation.target)) { + console.log(`[Settings] 上一次全局快捷键流程未完成,忽略本次触发: ${shortcut}`) return } - // 读取该快捷键的 autoCopy 配置,默认 false - const config = this.globalShortcutConfigs.get(shortcut) - const autoCopy = config?.autoCopy ?? false + this.isGlobalShortcutTriggering = true - console.log(`[Settings] 快捷键触发: ${shortcut}`) - console.log(`[Settings] 指令类型需要文本: ${preparation.shouldCaptureSelectedText}`) - console.log(`[Settings] 用户启用自动复制: ${autoCopy}`) + try { + // 读取该快捷键的 autoCopy 配置,默认 false + const config = this.globalShortcutConfigs.get(shortcut) + const autoCopy = config?.autoCopy ?? false + + console.log(`[Settings] 快捷键触发: ${shortcut}`) + console.log(`[Settings] 指令类型需要文本: ${preparation.shouldCaptureSelectedText}`) + console.log(`[Settings] 用户启用自动复制: ${autoCopy}`) - // 双重判断:指令类型需要文本 AND 用户启用自动复制 - const shouldCapture = preparation.shouldCaptureSelectedText && autoCopy + // 双重判断:指令类型需要文本 AND 用户启用自动复制 + const shouldCapture = preparation.shouldCaptureSelectedText && autoCopy - console.log(`[Settings] 最终是否执行取词: ${shouldCapture}`) + console.log(`[Settings] 最终是否执行取词: ${shouldCapture}`) - const context = shouldCapture ? await this.captureSelectedTextContext() : undefined - await this.handleGlobalShortcut(preparation.target, context) + const context = shouldCapture ? await this.captureSelectedTextContext() : undefined + await this.handleGlobalShortcut(preparation.target, context) + } finally { + this.isGlobalShortcutTriggering = false + } } /** * 判断某个快捷键目标是否允许在阻断期内再次触发。 - * 由于新的 native getSelectedContent() 方法不再需要等待,防抖逻辑已移除。 + * 若上一次全局快捷键流程尚未完成,直接忽略新的触发,避免重复取词和重复启动。 */ private shouldTriggerGlobalShortcut(_target: string): boolean { - return true + return !this.isGlobalShortcutTriggering } /** * 获取当前选中内容并转换成快捷键启动上下文。 - * 使用 native getSelectedContent() 方法,自动处理按键释放和剪贴板暂停。 + * 使用 native getSelectedContent() 方法,自动处理剪贴板暂停;调用前需等待修饰键释放。 */ private async captureSelectedTextContext(): Promise { console.log('[Settings] 开始捕获选中内容...') try { + const modifiersReleased = await doubleTapManager.waitForModifierKeysReleased() + if (!modifiersReleased) { + console.warn('[Settings] 修饰键未在限定时间内抬起,跳过本次取词') + return { + searchQuery: '', + pastedImage: null, + pastedFiles: null, + pastedText: null + } + } + const contents = NativeWindowManager.getSelectedContent() // 防御性检查:确保 contents 是有效数组 diff --git a/src/main/core/doubleTapManager.ts b/src/main/core/doubleTapManager.ts index 2e8c2d6c..d7c73712 100644 --- a/src/main/core/doubleTapManager.ts +++ b/src/main/core/doubleTapManager.ts @@ -37,6 +37,7 @@ class DoubleTapManager { private listenersRegistered = false private pressedKeycodes = new Set() private allKeysReleasedWaiters = new Set<() => void>() + private modifierKeysReleasedWaiters = new Set<(released: boolean) => void>() private keepAliveCount = 0 // 双击最大间隔(毫秒) @@ -113,6 +114,33 @@ class DoubleTapManager { }) } + /** + * 等待当前所有修饰键释放。 + * 全局快捷键触发后先等修饰键抬起,再模拟复制,避免修饰键影响 getSelectedContent。 + */ + waitForModifierKeysReleased(timeoutMs: number = 1000): Promise { + if (!this.hasPressedModifierKey()) { + return Promise.resolve(true) + } + + return new Promise((resolve) => { + let timer: ReturnType | null = setTimeout(() => { + this.modifierKeysReleasedWaiters.delete(wrappedResolve) + resolve(false) + }, timeoutMs) + + const wrappedResolve = (released: boolean) => { + if (timer) { + clearTimeout(timer) + timer = null + } + resolve(released) + } + + this.modifierKeysReleasedWaiters.add(wrappedResolve) + }) + } + private ensureStarted(): void { if (this.started) return this.started = true @@ -131,6 +159,9 @@ class DoubleTapManager { } } + /** + * 停止双击检测监听并释放所有等待按键抬起的调用方。 + */ private stop(): void { if (!this.started) return globalInputManager.release(INPUT_CONSUMER) @@ -142,6 +173,7 @@ class DoubleTapManager { this.modifierDownTime = 0 this.pressedKeycodes.clear() this.resolveAllKeysReleasedWaiters() + this.resolveModifierKeysReleasedWaiters(false) } private maybeStop(): void { @@ -167,6 +199,9 @@ class DoubleTapManager { } } + /** + * 处理全局 keyup 事件,维护按键状态并触发双击修饰键回调。 + */ private handleKeyUp(e: { keycode: number }): void { if (!this.started) return @@ -174,6 +209,9 @@ class DoubleTapManager { if (this.pressedKeycodes.size === 0) { this.resolveAllKeysReleasedWaiters() } + if (!this.hasPressedModifierKey()) { + this.resolveModifierKeysReleasedWaiters() + } const modifier = MODIFIER_KEYCODES[e.keycode] if (!modifier) { @@ -214,6 +252,21 @@ class DoubleTapManager { this.lastModifierUp = { modifier, time: now } } + /** + * 判断当前是否仍有修饰键处于按下状态。 + */ + private hasPressedModifierKey(): boolean { + for (const keycode of this.pressedKeycodes) { + if (MODIFIER_KEYCODES[keycode]) { + return true + } + } + return false + } + + /** + * 释放等待全部按键抬起的调用方。 + */ private resolveAllKeysReleasedWaiters(): void { if (this.allKeysReleasedWaiters.size === 0) { return @@ -225,6 +278,20 @@ class DoubleTapManager { this.allKeysReleasedWaiters.clear() } + /** + * 释放等待修饰键抬起的调用方。 + */ + private resolveModifierKeysReleasedWaiters(released: boolean = true): void { + if (this.modifierKeysReleasedWaiters.size === 0) { + return + } + + for (const resolve of this.modifierKeysReleasedWaiters) { + resolve(released) + } + this.modifierKeysReleasedWaiters.clear() + } + private fireHandlers(modifier: string): void { for (const handler of this.handlers) { if (handler.modifier === modifier) {