From edca554db1daaea05b202030108bd542d9d07b3d Mon Sep 17 00:00:00 2001 From: sudacode Date: Fri, 27 Feb 2026 00:34:34 -0800 Subject: [PATCH] small fixes --- src/core/services/overlay-window.ts | 2 +- src/main/overlay-runtime.test.ts | 49 +++++++++++++ src/main/overlay-runtime.ts | 72 ++++++++++++++----- .../runtime/overlay-runtime-bootstrap.test.ts | 2 +- src/main/runtime/overlay-runtime-bootstrap.ts | 8 ++- 5 files changed, 113 insertions(+), 20 deletions(-) diff --git a/src/core/services/overlay-window.ts b/src/core/services/overlay-window.ts index fdd1e36..e585ee2 100644 --- a/src/core/services/overlay-window.ts +++ b/src/core/services/overlay-window.ts @@ -117,7 +117,7 @@ export function createOverlayWindow( window.webContents.on('before-input-event', (event, input) => { if (kind === 'modal') return; - if (!options.isOverlayVisible(kind)) return; + if (!window.isVisible()) return; if (!options.tryHandleOverlayShortcutLocalFallback(input)) return; event.preventDefault(); }); diff --git a/src/main/overlay-runtime.test.ts b/src/main/overlay-runtime.test.ts index fcc4cf4..2e94652 100644 --- a/src/main/overlay-runtime.test.ts +++ b/src/main/overlay-runtime.test.ts @@ -225,6 +225,27 @@ test('handleOverlayModalClosed hides modal window only after all pending modals assert.equal(window.getHideCount(), 1); }); +test('sendToActiveOverlayWindow prefers visible main overlay window for modal open', () => { + const mainWindow = createMockWindow(); + mainWindow.visible = true; + const runtime = createOverlayModalRuntimeService({ + getMainWindow: () => mainWindow as never, + getModalWindow: () => null, + createModalWindow: () => { + throw new Error('modal window should not be created when main overlay is visible'); + }, + getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }), + setModalWindowBounds: () => {}, + }); + + const sent = runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, { + restoreOnModalClose: 'runtime-options', + }); + + assert.equal(sent, true); + assert.deepEqual(mainWindow.sent, [['runtime-options:open']]); +}); + test('modal runtime notifies callers when modal input state becomes active/inactive', () => { const window = createMockWindow(); const state: boolean[] = []; @@ -249,6 +270,8 @@ test('modal runtime notifies callers when modal input state becomes active/inact runtime.sendToActiveOverlayWindow('subsync:open-manual', { sourceTracks: [] }, { restoreOnModalClose: 'subsync', }); + assert.deepEqual(state, []); + runtime.notifyOverlayModalOpened('runtime-options'); assert.deepEqual(state, [true]); runtime.handleOverlayModalClosed('runtime-options'); @@ -258,6 +281,32 @@ test('modal runtime notifies callers when modal input state becomes active/inact assert.deepEqual(state, [true, false]); }); +test('handleOverlayModalClosed resets modal state even when modal window does not exist', () => { + const state: boolean[] = []; + const runtime = createOverlayModalRuntimeService( + { + getMainWindow: () => null, + getModalWindow: () => null, + createModalWindow: () => null, + getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }), + setModalWindowBounds: () => {}, + }, + { + onModalStateChange: (active: boolean): void => { + state.push(active); + }, + }, + ); + + runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, { + restoreOnModalClose: 'runtime-options', + }); + runtime.notifyOverlayModalOpened('runtime-options'); + runtime.handleOverlayModalClosed('runtime-options'); + + assert.deepEqual(state, [true, false]); +}); + test('handleOverlayModalClosed hides modal window for single kiku modal', () => { const window = createMockWindow(); const runtime = createOverlayModalRuntimeService({ diff --git a/src/main/overlay-runtime.ts b/src/main/overlay-runtime.ts index ca87c93..8f66128 100644 --- a/src/main/overlay-runtime.ts +++ b/src/main/overlay-runtime.ts @@ -65,13 +65,19 @@ export function createOverlayModalRuntimeService( return null; }; -const isWindowReadyForIpc = (window: BrowserWindow): boolean => { - if (window.webContents.isLoading()) { - return false; - } - const currentURL = window.webContents.getURL(); - return currentURL !== '' && currentURL !== 'about:blank'; -}; + const isWindowReadyForIpc = (window: BrowserWindow): boolean => { + if (window.webContents.isLoading()) { + return false; + } + const currentURL = window.webContents.getURL(); + return currentURL !== '' && currentURL !== 'about:blank'; + }; + + const elevateModalWindow = (window: BrowserWindow): void => { + if (window.isDestroyed()) return; + window.setAlwaysOnTop(true, 'screen-saver', 1); + window.moveTop(); + }; const sendOrQueueForWindow = ( window: BrowserWindow, @@ -95,7 +101,10 @@ const isWindowReadyForIpc = (window: BrowserWindow): boolean => { passThroughMouseEvents: boolean; } = { passThroughMouseEvents: false }, ): void => { - window.show(); + if (!window.isVisible()) { + window.show(); + } + elevateModalWindow(window); if (options.passThroughMouseEvents) { window.setIgnoreMouseEvents(true, { forward: true }); } else { @@ -107,6 +116,22 @@ const isWindowReadyForIpc = (window: BrowserWindow): boolean => { } }; + const ensureModalWindowInteractive = (window: BrowserWindow): void => { + if (window.isVisible()) { + window.setIgnoreMouseEvents(false); + if (!window.isFocused()) { + window.focus(); + } + if (!window.webContents.isFocused()) { + window.webContents.focus(); + } + elevateModalWindow(window); + return; + } + + showModalWindow(window); + }; + const showOverlayWindowForModal = (window: BrowserWindow): void => { window.show(); if (!window.isFocused()) { @@ -137,7 +162,7 @@ const isWindowReadyForIpc = (window: BrowserWindow): boolean => { if (!targetWindow || targetWindow.isDestroyed() || targetWindow.isVisible()) { return; } - showModalWindow(targetWindow, { passThroughMouseEvents: true }); + showModalWindow(targetWindow, { passThroughMouseEvents: false }); }, MODAL_REVEAL_FALLBACK_DELAY_MS); }; @@ -149,6 +174,7 @@ const isWindowReadyForIpc = (window: BrowserWindow): boolean => { const restoreOnModalClose = runtimeOptions?.restoreOnModalClose; const sendNow = (window: BrowserWindow): void => { + ensureModalWindowInteractive(window); if (payload === undefined) { window.webContents.send(channel); } else { @@ -157,17 +183,24 @@ const isWindowReadyForIpc = (window: BrowserWindow): boolean => { }; if (restoreOnModalClose) { + restoreVisibleOverlayOnModalClose.add(restoreOnModalClose); + const mainWindow = getTargetOverlayWindow(); + if (mainWindow && !mainWindow.isDestroyed() && mainWindow.isVisible()) { + sendOrQueueForWindow(mainWindow, (window) => { + if (payload === undefined) { + window.webContents.send(channel); + } else { + window.webContents.send(channel, payload); + } + }); + return true; + } + const modalWindow = resolveModalWindow(); if (!modalWindow) return false; deps.setModalWindowBounds(deps.getModalGeometry()); const wasVisible = modalWindow.isVisible(); - const wasModalActive = restoreVisibleOverlayOnModalClose.size > 0; - restoreVisibleOverlayOnModalClose.add(restoreOnModalClose); - if (!wasModalActive) { - notifyModalStateChange(true); - } - if (!wasVisible) { scheduleModalWindowReveal(modalWindow); } else if (!modalWindow.isFocused()) { @@ -199,17 +232,21 @@ const isWindowReadyForIpc = (window: BrowserWindow): boolean => { const handleOverlayModalClosed = (modal: OverlayHostedModal): void => { if (!restoreVisibleOverlayOnModalClose.has(modal)) return; restoreVisibleOverlayOnModalClose.delete(modal); - const modalWindow = deps.getModalWindow(); - if (!modalWindow || modalWindow.isDestroyed()) return; if (restoreVisibleOverlayOnModalClose.size === 0) { clearPendingModalWindowReveal(); notifyModalStateChange(false); + } + + const modalWindow = deps.getModalWindow(); + if (!modalWindow || modalWindow.isDestroyed()) return; + if (restoreVisibleOverlayOnModalClose.size === 0) { modalWindow.hide(); } }; const notifyOverlayModalOpened = (modal: OverlayHostedModal): void => { if (!restoreVisibleOverlayOnModalClose.has(modal)) return; + notifyModalStateChange(true); const targetWindow = deps.getModalWindow(); clearPendingModalWindowReveal(); if (!targetWindow || targetWindow.isDestroyed()) { @@ -218,6 +255,7 @@ const isWindowReadyForIpc = (window: BrowserWindow): boolean => { if (targetWindow.isVisible()) { targetWindow.setIgnoreMouseEvents(false); + elevateModalWindow(targetWindow); if (!targetWindow.isFocused()) { targetWindow.focus(); } diff --git a/src/main/runtime/overlay-runtime-bootstrap.test.ts b/src/main/runtime/overlay-runtime-bootstrap.test.ts index 890313d..82256d7 100644 --- a/src/main/runtime/overlay-runtime-bootstrap.test.ts +++ b/src/main/runtime/overlay-runtime-bootstrap.test.ts @@ -41,5 +41,5 @@ test('overlay runtime bootstrap runs core init and applies post-init state', () initialize(); assert.equal(initialized, true); - assert.deepEqual(calls, ['options', 'core', 'initialized:yes', 'warmups']); + assert.deepEqual(calls, ['options', 'initialized:yes', 'core', 'warmups']); }); diff --git a/src/main/runtime/overlay-runtime-bootstrap.ts b/src/main/runtime/overlay-runtime-bootstrap.ts index 8bac07b..74c4420 100644 --- a/src/main/runtime/overlay-runtime-bootstrap.ts +++ b/src/main/runtime/overlay-runtime-bootstrap.ts @@ -41,8 +41,14 @@ export function createInitializeOverlayRuntimeHandler(deps: { }) { return (): void => { if (deps.isOverlayRuntimeInitialized()) return; - deps.initializeOverlayRuntimeCore(deps.buildOptions()); + const options = deps.buildOptions(); deps.setOverlayRuntimeInitialized(true); + try { + deps.initializeOverlayRuntimeCore(options); + } catch (error) { + deps.setOverlayRuntimeInitialized(false); + throw error; + } deps.startBackgroundWarmups(); }; }