Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 34 additions & 14 deletions src/main/api/renderer/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export class SettingsAPI {
// 全局快捷键配置映射(存储每个快捷键的 autoCopy 等配置)
private globalShortcutConfigs: Map<string, { autoCopy: boolean }> = new Map()
private globalShortcutKeyboardStateReleasers = new Map<string, () => void>()
// 全局快捷键触发流程执行中时,后续触发会被忽略,避免重复复制和重复启动。
private isGlobalShortcutTriggering = false

private setupIPC(): void {
// 主题
Expand Down Expand Up @@ -379,41 +381,59 @@ export class SettingsAPI {
preparation: GlobalShortcutPreparation
): Promise<void> {
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<ShortcutLaunchContext> {
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 是有效数组
Expand Down
67 changes: 67 additions & 0 deletions src/main/core/doubleTapManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class DoubleTapManager {
private listenersRegistered = false
private pressedKeycodes = new Set<number>()
private allKeysReleasedWaiters = new Set<() => void>()
private modifierKeysReleasedWaiters = new Set<(released: boolean) => void>()
private keepAliveCount = 0

// 双击最大间隔(毫秒)
Expand Down Expand Up @@ -113,6 +114,33 @@ class DoubleTapManager {
})
}

/**
* 等待当前所有修饰键释放。
* 全局快捷键触发后先等修饰键抬起,再模拟复制,避免修饰键影响 getSelectedContent。
*/
waitForModifierKeysReleased(timeoutMs: number = 1000): Promise<boolean> {
if (!this.hasPressedModifierKey()) {
return Promise.resolve(true)
}

return new Promise((resolve) => {
let timer: ReturnType<typeof setTimeout> | 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)
})
}
Comment on lines +121 to +142

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

waitForModifierKeysReleased 方法中,wrappedResolvesetTimeout 的回调函数中被引用,但它的声明(const wrappedResolve = ...)却在 setTimeout 调用之后。虽然在运行时因为 setTimeout 是异步执行的,所以不会导致 Temporal Dead Zone (TDZ) 报错,但这种“在声明前引用”的代码结构不符合词法作用域的最佳实践,并且容易触发静态代码分析工具或 Linter(如 no-use-before-define)的警告。\n\n建议通过先声明 timer,再声明 wrappedResolve,最后为 timer 赋值的方式,优雅地解决这个循环引用的问题。

  waitForModifierKeysReleased(timeoutMs: number = 1000): Promise<boolean> {\n    if (!this.hasPressedModifierKey()) {\n      return Promise.resolve(true)\n    }\n\n    return new Promise((resolve) => {\n      let timer: ReturnType<typeof setTimeout> | null = null\n\n      const wrappedResolve = (released: boolean) => {\n        if (timer) {\n          clearTimeout(timer)\n          timer = null\n        }\n        resolve(released)\n      }\n\n      timer = setTimeout(() => {\n        this.modifierKeysReleasedWaiters.delete(wrappedResolve)\n        resolve(false)\n      }, timeoutMs)\n\n      this.modifierKeysReleasedWaiters.add(wrappedResolve)\n    })\n  }


private ensureStarted(): void {
if (this.started) return
this.started = true
Expand All @@ -131,6 +159,9 @@ class DoubleTapManager {
}
}

/**
* 停止双击检测监听并释放所有等待按键抬起的调用方。
*/
private stop(): void {
if (!this.started) return
globalInputManager.release(INPUT_CONSUMER)
Expand All @@ -142,6 +173,7 @@ class DoubleTapManager {
this.modifierDownTime = 0
this.pressedKeycodes.clear()
this.resolveAllKeysReleasedWaiters()
this.resolveModifierKeysReleasedWaiters(false)
}

private maybeStop(): void {
Expand All @@ -167,13 +199,19 @@ class DoubleTapManager {
}
}

/**
* 处理全局 keyup 事件,维护按键状态并触发双击修饰键回调。
*/
private handleKeyUp(e: { keycode: number }): void {
if (!this.started) return

this.pressedKeycodes.delete(e.keycode)
if (this.pressedKeycodes.size === 0) {
this.resolveAllKeysReleasedWaiters()
}
if (!this.hasPressedModifierKey()) {
this.resolveModifierKeysReleasedWaiters()
}

const modifier = MODIFIER_KEYCODES[e.keycode]
if (!modifier) {
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down