From 3f57851abde4b8b946a9e6144c874427f95da033 Mon Sep 17 00:00:00 2001 From: xiehui Date: Mon, 1 Jun 2026 19:57:11 +0800 Subject: [PATCH 1/3] fix: keep plugin file dialogs from hiding window --- src/main/api/plugin/dialog.ts | 9 +++++++-- src/main/managers/windowManager.ts | 30 +++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/main/api/plugin/dialog.ts b/src/main/api/plugin/dialog.ts index e68ffd3d..c968760f 100644 --- a/src/main/api/plugin/dialog.ts +++ b/src/main/api/plugin/dialog.ts @@ -1,5 +1,6 @@ import { ipcMain, dialog, app } from 'electron' import detachedWindowManager from '../../core/detachedWindowManager' +import windowManager from '../../managers/windowManager' /** * 对话框API - 插件专用 @@ -75,7 +76,9 @@ export class PluginDialogAPI { event.returnValue = undefined return } - const result = dialog.showSaveDialogSync(targetWindow, options) + const result = windowManager.withBlurHideSuppressed(() => + dialog.showSaveDialogSync(targetWindow, options) + ) event.returnValue = result } catch (error) { console.error('[PluginDialog] 显示文件保存对话框失败:', error) @@ -94,7 +97,9 @@ export class PluginDialogAPI { event.returnValue = [] return } - const result = dialog.showOpenDialogSync(targetWindow, options) + const result = windowManager.withBlurHideSuppressed(() => + dialog.showOpenDialogSync(targetWindow, options) + ) event.returnValue = result || [] } catch (error) { console.error('[PluginDialog] 显示文件打开对话框失败:', error) diff --git a/src/main/managers/windowManager.ts b/src/main/managers/windowManager.ts index 1d64e494..b24c68d7 100644 --- a/src/main/managers/windowManager.ts +++ b/src/main/managers/windowManager.ts @@ -83,6 +83,9 @@ class WindowManager { private lastFocusTarget: 'mainWindow' | 'plugin' | null = null // 窗口隐藏前的焦点状态 private isRestoringFocus: boolean = false // 是否正在恢复焦点状态(防止 focus 事件监听器干扰) private suppressBlurHide: boolean = false // 临时抑制 blur 事件隐藏窗口(文件关联打开等场景) + // Native modal dialogs can emit queued blur/mouseup events around close. + private modalDialogBlurHideSuppressed: boolean = false + private modalDialogBlurHideReleaseTimer: ReturnType | null = null private lastBlurHideTime: number = 0 // blur 导致隐藏窗口的时间戳(用于解决托盘点击竞态) private blurHideTimer: ReturnType | null = null // Linux blur 延迟隐藏定时器 // Double-tap 唤醒窗口时,Windows 可能紧跟一个短暂 blur;这两个 timer 用于跳过误关闭并补一次焦点。 @@ -142,6 +145,10 @@ class WindowManager { } } + private isBlurHideSuppressed(): boolean { + return this.suppressBlurHide || this.modalDialogBlurHideSuppressed + } + private deferBlurHideUntilMouseUp(): void { this.pendingBlurHideOnMouseUp = true this.clearPendingBlurHideTimer() @@ -152,6 +159,7 @@ class WindowManager { if (!this.pendingBlurHideOnMouseUp) return this.pendingBlurHideOnMouseUp = false + if (this.isBlurHideSuppressed()) return if (this.mainWindow?.isFocused()) return if (pluginManager.isPluginViewFocused()) return @@ -169,6 +177,7 @@ class WindowManager { private resolveMouseUpVisibility(): void { if (!this.mainWindow?.isVisible()) return + if (this.isBlurHideSuppressed()) return // 拖拽最终落在窗口内时保持窗口;落在窗口外时按普通外部点击处理并关闭。 const cursorPoint = screen.getCursorScreenPoint() @@ -392,7 +401,7 @@ class WindowManager { }) this.mainWindow.on('blur', () => { - if (this.suppressBlurHide) return + if (this.isBlurHideSuppressed()) return // 左键仍按下时可能是从外部拖文件进窗口,先等 mouseup 再决定是否隐藏。 if (this.leftMouseDown) { @@ -409,6 +418,7 @@ class WindowManager { } this.blurHideTimer = setTimeout(() => { this.blurHideTimer = null + if (this.isBlurHideSuppressed()) return // 主窗口重新获焦 → 不隐藏 if (this.mainWindow?.isFocused()) return // 插件视图持有焦点(应用内部切换)→ 不隐藏 @@ -853,6 +863,24 @@ class WindowManager { this.startAutoBackToSearchTimer() } + public withBlurHideSuppressed(callback: () => T, releaseDelayMs: number = 500): T { + if (this.modalDialogBlurHideReleaseTimer) { + clearTimeout(this.modalDialogBlurHideReleaseTimer) + this.modalDialogBlurHideReleaseTimer = null + } + + this.modalDialogBlurHideSuppressed = true + + try { + return callback() + } finally { + this.modalDialogBlurHideReleaseTimer = setTimeout(() => { + this.modalDialogBlurHideSuppressed = false + this.modalDialogBlurHideReleaseTimer = null + }, releaseDelayMs) + } + } + /** * 启动自动返回搜索定时器 */ From 99047c2857e9f473f7778b9cd0b6098b530e6e98 Mon Sep 17 00:00:00 2001 From: xiehui Date: Mon, 1 Jun 2026 20:37:37 +0800 Subject: [PATCH 2/3] fix: harden modal dialog blur suppression --- src/main/managers/windowManager.ts | 69 ++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/src/main/managers/windowManager.ts b/src/main/managers/windowManager.ts index b24c68d7..bbb89545 100644 --- a/src/main/managers/windowManager.ts +++ b/src/main/managers/windowManager.ts @@ -86,6 +86,7 @@ class WindowManager { // Native modal dialogs can emit queued blur/mouseup events around close. private modalDialogBlurHideSuppressed: boolean = false private modalDialogBlurHideReleaseTimer: ReturnType | null = null + private modalDialogBlurHideSuppressionDepth: number = 0 private lastBlurHideTime: number = 0 // blur 导致隐藏窗口的时间戳(用于解决托盘点击竞态) private blurHideTimer: ReturnType | null = null // Linux blur 延迟隐藏定时器 // Double-tap 唤醒窗口时,Windows 可能紧跟一个短暂 blur;这两个 timer 用于跳过误关闭并补一次焦点。 @@ -149,6 +150,41 @@ class WindowManager { return this.suppressBlurHide || this.modalDialogBlurHideSuppressed } + private beginModalDialogBlurHideSuppression(): void { + if (this.modalDialogBlurHideReleaseTimer) { + clearTimeout(this.modalDialogBlurHideReleaseTimer) + this.modalDialogBlurHideReleaseTimer = null + } + + this.modalDialogBlurHideSuppressionDepth += 1 + this.modalDialogBlurHideSuppressed = true + } + + private endModalDialogBlurHideSuppression(releaseDelayMs: number): void { + this.modalDialogBlurHideSuppressionDepth = Math.max( + 0, + this.modalDialogBlurHideSuppressionDepth - 1 + ) + if (this.modalDialogBlurHideSuppressionDepth > 0) return + + if (this.modalDialogBlurHideReleaseTimer) { + clearTimeout(this.modalDialogBlurHideReleaseTimer) + } + + this.modalDialogBlurHideReleaseTimer = setTimeout(() => { + this.modalDialogBlurHideSuppressed = false + this.modalDialogBlurHideReleaseTimer = null + }, releaseDelayMs) + } + + private isPromiseLike(value: T | PromiseLike): value is PromiseLike { + return ( + value !== null && + (typeof value === 'object' || typeof value === 'function') && + typeof (value as { then?: unknown }).then === 'function' + ) + } + private deferBlurHideUntilMouseUp(): void { this.pendingBlurHideOnMouseUp = true this.clearPendingBlurHideTimer() @@ -863,21 +899,26 @@ class WindowManager { this.startAutoBackToSearchTimer() } - public withBlurHideSuppressed(callback: () => T, releaseDelayMs: number = 500): T { - if (this.modalDialogBlurHideReleaseTimer) { - clearTimeout(this.modalDialogBlurHideReleaseTimer) - this.modalDialogBlurHideReleaseTimer = null - } - - this.modalDialogBlurHideSuppressed = true - + public withBlurHideSuppressed(callback: () => PromiseLike, releaseDelayMs?: number): Promise + public withBlurHideSuppressed(callback: () => T, releaseDelayMs?: number): T + public withBlurHideSuppressed( + callback: () => T | PromiseLike, + releaseDelayMs: number = 500 + ): T | Promise { + this.beginModalDialogBlurHideSuppression() try { - return callback() - } finally { - this.modalDialogBlurHideReleaseTimer = setTimeout(() => { - this.modalDialogBlurHideSuppressed = false - this.modalDialogBlurHideReleaseTimer = null - }, releaseDelayMs) + const result = callback() + if (this.isPromiseLike(result)) { + return Promise.resolve(result).finally(() => { + this.endModalDialogBlurHideSuppression(releaseDelayMs) + }) + } + + this.endModalDialogBlurHideSuppression(releaseDelayMs) + return result + } catch (error) { + this.endModalDialogBlurHideSuppression(releaseDelayMs) + throw error } } From 20e45862e15d0b07bd3239d2bba9a15dab7b7638 Mon Sep 17 00:00:00 2001 From: xiehui Date: Mon, 1 Jun 2026 22:33:55 +0800 Subject: [PATCH 3/3] fix: address dialog blur review comments --- src/main/api/plugin/dialog.ts | 4 ++-- src/main/managers/windowManager.ts | 29 ++++++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/main/api/plugin/dialog.ts b/src/main/api/plugin/dialog.ts index c968760f..c88bfaf2 100644 --- a/src/main/api/plugin/dialog.ts +++ b/src/main/api/plugin/dialog.ts @@ -76,7 +76,7 @@ export class PluginDialogAPI { event.returnValue = undefined return } - const result = windowManager.withBlurHideSuppressed(() => + const result = windowManager.withBlurHideSuppressedSync(() => dialog.showSaveDialogSync(targetWindow, options) ) event.returnValue = result @@ -97,7 +97,7 @@ export class PluginDialogAPI { event.returnValue = [] return } - const result = windowManager.withBlurHideSuppressed(() => + const result = windowManager.withBlurHideSuppressedSync(() => dialog.showOpenDialogSync(targetWindow, options) ) event.returnValue = result || [] diff --git a/src/main/managers/windowManager.ts b/src/main/managers/windowManager.ts index bbb89545..109e049e 100644 --- a/src/main/managers/windowManager.ts +++ b/src/main/managers/windowManager.ts @@ -30,6 +30,7 @@ import pluginManager from './pluginManager' // 窗口材质类型 type WindowMaterial = 'mica' | 'acrylic' | 'none' const WINDOW_BLUR_DRAG_INPUT_CONSUMER = 'window-blur-drag' +const DEFAULT_MODAL_DIALOG_BLUR_HIDE_RELEASE_DELAY_MS = 500 /** * 应用快捷键触发时携带的文件输入 @@ -83,7 +84,7 @@ class WindowManager { private lastFocusTarget: 'mainWindow' | 'plugin' | null = null // 窗口隐藏前的焦点状态 private isRestoringFocus: boolean = false // 是否正在恢复焦点状态(防止 focus 事件监听器干扰) private suppressBlurHide: boolean = false // 临时抑制 blur 事件隐藏窗口(文件关联打开等场景) - // Native modal dialogs can emit queued blur/mouseup events around close. + // 原生模态对话框关闭前后可能发出排队的 blur/mouseup 事件。 private modalDialogBlurHideSuppressed: boolean = false private modalDialogBlurHideReleaseTimer: ReturnType | null = null private modalDialogBlurHideSuppressionDepth: number = 0 @@ -899,11 +900,14 @@ class WindowManager { this.startAutoBackToSearchTimer() } - public withBlurHideSuppressed(callback: () => PromiseLike, releaseDelayMs?: number): Promise + public withBlurHideSuppressed( + callback: () => PromiseLike, + releaseDelayMs?: number + ): Promise public withBlurHideSuppressed(callback: () => T, releaseDelayMs?: number): T public withBlurHideSuppressed( callback: () => T | PromiseLike, - releaseDelayMs: number = 500 + releaseDelayMs: number = DEFAULT_MODAL_DIALOG_BLUR_HIDE_RELEASE_DELAY_MS ): T | Promise { this.beginModalDialogBlurHideSuppression() try { @@ -922,6 +926,25 @@ class WindowManager { } } + public withBlurHideSuppressedSync( + callback: () => T, + releaseDelayMs: number = DEFAULT_MODAL_DIALOG_BLUR_HIDE_RELEASE_DELAY_MS + ): T { + this.beginModalDialogBlurHideSuppression() + try { + const result = callback() + if (this.isPromiseLike(result)) { + throw new TypeError('withBlurHideSuppressedSync callback must not return a Promise') + } + + this.endModalDialogBlurHideSuppression(releaseDelayMs) + return result + } catch (error) { + this.endModalDialogBlurHideSuppression(releaseDelayMs) + throw error + } + } + /** * 启动自动返回搜索定时器 */