@@ -40,6 +40,8 @@ export class WindowPresenter implements IWindowPresenter {
4040 > ( )
4141 private floatingChatWindow : FloatingChatWindow | null = null
4242 private settingsWindow : BrowserWindow | null = null
43+ private tooltipOverlayWindows = new Map < number , BrowserWindow > ( )
44+ private pendingTooltipPayload = new Map < number , { x : number ; y : number ; text : string } > ( )
4345
4446 constructor ( configPresenter : IConfigPresenter ) {
4547 this . windows = new Map ( )
@@ -66,6 +68,44 @@ export class WindowPresenter implements IWindowPresenter {
6668 }
6769 } )
6870
71+ ipcMain . on (
72+ 'shell-tooltip:show' ,
73+ ( event , payload : { x : number ; y : number ; text : string } | undefined ) => {
74+ if ( ! payload ) return
75+
76+ const parentWindow = BrowserWindow . fromWebContents ( event . sender )
77+ if ( ! parentWindow || parentWindow . isDestroyed ( ) ) return
78+
79+ const overlay = this . getOrCreateTooltipOverlay ( parentWindow )
80+ if ( ! overlay ) return
81+
82+ this . pendingTooltipPayload . set ( parentWindow . id , payload )
83+
84+ if ( ! overlay . webContents . isLoadingMainFrame ( ) ) {
85+ overlay . webContents . send ( 'shell-tooltip-overlay:show' , payload )
86+ return
87+ }
88+
89+ overlay . webContents . once ( 'did-finish-load' , ( ) => {
90+ const pending = this . pendingTooltipPayload . get ( parentWindow . id )
91+ if ( ! pending ) return
92+ if ( overlay . isDestroyed ( ) ) return
93+ overlay . webContents . send ( 'shell-tooltip-overlay:show' , pending )
94+ } )
95+ }
96+ )
97+
98+ ipcMain . on ( 'shell-tooltip:hide' , ( event ) => {
99+ const parentWindow = BrowserWindow . fromWebContents ( event . sender )
100+ if ( ! parentWindow || parentWindow . isDestroyed ( ) ) return
101+
102+ const overlay = this . tooltipOverlayWindows . get ( parentWindow . id )
103+ if ( ! overlay || overlay . isDestroyed ( ) ) return
104+
105+ this . pendingTooltipPayload . delete ( parentWindow . id )
106+ overlay . webContents . send ( 'shell-tooltip-overlay:hide' )
107+ } )
108+
69109 // Listen for shortcut event: create new window
70110 eventBus . on ( SHORTCUT_EVENTS . CREATE_NEW_WINDOW , ( ) => {
71111 console . log ( 'Creating new shell window via shortcut.' )
@@ -734,6 +774,7 @@ export class WindowPresenter implements IWindowPresenter {
734774 if ( ! shellWindow . isDestroyed ( ) ) {
735775 shellWindow . webContents . send ( 'window-blurred' , windowId )
736776 }
777+ this . clearTooltipOverlay ( windowId )
737778 } )
738779
739780 // 窗口最大化
@@ -881,6 +922,7 @@ export class WindowPresenter implements IWindowPresenter {
881922 this . windowFocusStates . delete ( windowIdBeingClosed )
882923 shellWindowState . unmanage ( ) // 停止管理窗口状态
883924 eventBus . sendToMain ( WINDOW_EVENTS . WINDOW_CLOSED , windowIdBeingClosed )
925+ this . destroyTooltipOverlay ( windowIdBeingClosed )
884926 console . log (
885927 `Window ${ windowIdBeingClosed } closed event handled. Map size AFTER delete: ${ this . windows . size } `
886928 )
@@ -911,6 +953,12 @@ export class WindowPresenter implements IWindowPresenter {
911953 shellWindow . loadFile ( join ( __dirname , '../renderer/shell/index.html' ) )
912954 }
913955
956+ // Pre-create tooltip overlay so first hover is instant
957+ shellWindow . webContents . once ( 'did-finish-load' , ( ) => {
958+ if ( shellWindow . isDestroyed ( ) ) return
959+ this . getOrCreateTooltipOverlay ( shellWindow )
960+ } )
961+
914962 // --- 处理初始标签页创建或激活 ---
915963
916964 // 如果提供了 options?.initialTab,等待窗口加载完成,然后创建新标签页
@@ -982,6 +1030,117 @@ export class WindowPresenter implements IWindowPresenter {
9821030 return windowId // 返回新创建窗口的 ID
9831031 }
9841032
1033+ private getOrCreateTooltipOverlay ( parentWindow : BrowserWindow ) : BrowserWindow | null {
1034+ if ( parentWindow . isDestroyed ( ) ) return null
1035+
1036+ const existing = this . tooltipOverlayWindows . get ( parentWindow . id )
1037+ if ( existing && ! existing . isDestroyed ( ) ) {
1038+ this . syncTooltipOverlayBounds ( parentWindow , existing )
1039+ if ( ! existing . isVisible ( ) ) {
1040+ existing . showInactive ( )
1041+ }
1042+ return existing
1043+ }
1044+
1045+ const bounds = parentWindow . getContentBounds ( )
1046+
1047+ const overlay = new BrowserWindow ( {
1048+ x : bounds . x ,
1049+ y : bounds . y ,
1050+ width : bounds . width ,
1051+ height : bounds . height ,
1052+ parent : parentWindow ,
1053+ show : false ,
1054+ frame : false ,
1055+ transparent : true ,
1056+ backgroundColor : '#00000000' ,
1057+ resizable : false ,
1058+ movable : false ,
1059+ minimizable : false ,
1060+ maximizable : false ,
1061+ closable : false ,
1062+ hasShadow : false ,
1063+ focusable : false ,
1064+ skipTaskbar : true ,
1065+ autoHideMenuBar : true ,
1066+ webPreferences : {
1067+ preload : join ( __dirname , '../preload/index.mjs' ) ,
1068+ sandbox : false ,
1069+ devTools : is . dev
1070+ }
1071+ } )
1072+
1073+ overlay . setIgnoreMouseEvents ( true , { forward : true } )
1074+
1075+ const sync = ( ) => {
1076+ const current = this . tooltipOverlayWindows . get ( parentWindow . id )
1077+ if ( ! current || current . isDestroyed ( ) || parentWindow . isDestroyed ( ) ) return
1078+ this . syncTooltipOverlayBounds ( parentWindow , current )
1079+ }
1080+
1081+ parentWindow . on ( 'move' , sync )
1082+ parentWindow . on ( 'resize' , sync )
1083+ parentWindow . on ( 'show' , ( ) => {
1084+ if ( ! overlay . isDestroyed ( ) ) overlay . showInactive ( )
1085+ } )
1086+ parentWindow . on ( 'hide' , ( ) => {
1087+ if ( ! overlay . isDestroyed ( ) ) overlay . hide ( )
1088+ } )
1089+ parentWindow . on ( 'minimize' , ( ) => {
1090+ if ( ! overlay . isDestroyed ( ) ) overlay . hide ( )
1091+ } )
1092+ parentWindow . on ( 'restore' , ( ) => {
1093+ if ( ! overlay . isDestroyed ( ) ) overlay . showInactive ( )
1094+ } )
1095+
1096+ overlay . on ( 'closed' , ( ) => {
1097+ this . tooltipOverlayWindows . delete ( parentWindow . id )
1098+ this . pendingTooltipPayload . delete ( parentWindow . id )
1099+ } )
1100+
1101+ if ( is . dev && process . env [ 'ELECTRON_RENDERER_URL' ] ) {
1102+ overlay . loadURL ( process . env [ 'ELECTRON_RENDERER_URL' ] + '/shell/tooltip-overlay/index.html' )
1103+ } else {
1104+ overlay . loadFile ( join ( __dirname , '../renderer/shell/tooltip-overlay/index.html' ) )
1105+ }
1106+
1107+ overlay . webContents . once ( 'did-finish-load' , ( ) => {
1108+ if ( overlay . isDestroyed ( ) ) return
1109+ overlay . showInactive ( )
1110+ overlay . webContents . send ( 'shell-tooltip-overlay:clear' )
1111+
1112+ const pending = this . pendingTooltipPayload . get ( parentWindow . id )
1113+ if ( pending ) {
1114+ overlay . webContents . send ( 'shell-tooltip-overlay:show' , pending )
1115+ }
1116+ } )
1117+
1118+ this . tooltipOverlayWindows . set ( parentWindow . id , overlay )
1119+ return overlay
1120+ }
1121+
1122+ private syncTooltipOverlayBounds ( parentWindow : BrowserWindow , overlay : BrowserWindow ) : void {
1123+ if ( parentWindow . isDestroyed ( ) || overlay . isDestroyed ( ) ) return
1124+ const bounds = parentWindow . getContentBounds ( )
1125+ overlay . setBounds ( bounds )
1126+ }
1127+
1128+ private clearTooltipOverlay ( windowId : number ) : void {
1129+ const overlay = this . tooltipOverlayWindows . get ( windowId )
1130+ if ( ! overlay || overlay . isDestroyed ( ) ) return
1131+ this . pendingTooltipPayload . delete ( windowId )
1132+ overlay . webContents . send ( 'shell-tooltip-overlay:hide' )
1133+ }
1134+
1135+ private destroyTooltipOverlay ( windowId : number ) : void {
1136+ const overlay = this . tooltipOverlayWindows . get ( windowId )
1137+ if ( overlay && ! overlay . isDestroyed ( ) ) {
1138+ overlay . destroy ( )
1139+ }
1140+ this . tooltipOverlayWindows . delete ( windowId )
1141+ this . pendingTooltipPayload . delete ( windowId )
1142+ }
1143+
9851144 /**
9861145 * 更新指定窗口的内容保护设置。
9871146 * @param window BrowserWindow 实例。
0 commit comments