From 1a2f6a8b7f42f25993aeda32b6f7cde889b83055 Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Sat, 5 Sep 2026 23:13:02 +0200 Subject: [PATCH 1/4] fix: keep the Windows popup above the tray overflow Use the pop-up-menu stacking level on Windows and control click-away dismissal separately. Preserve pin preferences and DevTools behavior. Requires the unreleased electron-menubar hideOnBlur option. Validated against the linked local library; update the dependency before publishing. Related: #1048 --- src/main/lifecycle/window.test.ts | 43 +++++++++++++++++++++++++++++++ src/main/lifecycle/window.ts | 37 +++++++++++++++++--------- 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/main/lifecycle/window.test.ts b/src/main/lifecycle/window.test.ts index 12d42ad28..89d93405b 100644 --- a/src/main/lifecycle/window.test.ts +++ b/src/main/lifecycle/window.test.ts @@ -65,6 +65,7 @@ describe('main/lifecycle/window.ts', () => { setPlatform('linux'); menubar = { + setOption: vi.fn(), hideWindow: vi.fn(), recenterOnTray: vi.fn(), tray: { @@ -81,6 +82,7 @@ describe('main/lifecycle/window.ts', () => { on: vi.fn(), webContents: { on: vi.fn(), + isDevToolsOpened: vi.fn().mockReturnValue(false), }, }, } as unknown as Menubar; @@ -152,6 +154,47 @@ describe('main/lifecycle/window.ts', () => { }); describe('applyKeepWindowOnBlur', () => { + it.each([false, true])('keeps Windows above the tray with keep-open set to %s', (keepOpen) => { + setPlatform('win32'); + + applyKeepWindowOnBlur(menubar, keepOpen); + + expect(menubar.window?.setAlwaysOnTop).toHaveBeenCalledWith(true, 'pop-up-menu'); + expect(menubar.setOption).toHaveBeenCalledWith('hideOnBlur', !keepOpen); + }); + + it('raises the Windows popup before its first tray click', () => { + setPlatform('win32'); + + configureWindowEvents(menubar, menuBuilder); + + expect(menubar.window?.setAlwaysOnTop).toHaveBeenCalledWith(true, 'pop-up-menu'); + expect(menubar.setOption).toHaveBeenCalledWith('hideOnBlur', true); + }); + + it('preserves Windows stacking and restores click-away after DevTools closes', () => { + setPlatform('win32'); + configureWindowEvents(menubar, menuBuilder); + + findWebContentsHandler(menubar, 'devtools-opened')?.(); + expect(menubar.setOption).toHaveBeenLastCalledWith('hideOnBlur', false); + expect(menubar.window?.setAlwaysOnTop).toHaveBeenLastCalledWith(true, 'pop-up-menu'); + + findWebContentsHandler(menubar, 'devtools-closed')?.(); + expect(menubar.setOption).toHaveBeenLastCalledWith('hideOnBlur', true); + expect(menubar.window?.setAlwaysOnTop).toHaveBeenLastCalledWith(true, 'pop-up-menu'); + }); + + it('keeps DevTools open when the keep-open setting changes', () => { + const webContents = menubar.window!.webContents; + vi.mocked(webContents.isDevToolsOpened).mockReturnValue(true); + + applyKeepWindowOnBlur(menubar, false); + + expect(menubar.setOption).toHaveBeenCalledWith('hideOnBlur', false); + expect(menubar.window?.setAlwaysOnTop).toHaveBeenCalledWith(true); + }); + it('forwards the value to the underlying window', () => { applyKeepWindowOnBlur(menubar, true); diff --git a/src/main/lifecycle/window.ts b/src/main/lifecycle/window.ts index 2694722b4..ad25a708f 100644 --- a/src/main/lifecycle/window.ts +++ b/src/main/lifecycle/window.ts @@ -1,7 +1,7 @@ import { app } from 'electron'; import type { Menubar } from 'electron-menubar'; -import { isMacOS } from '../../shared/platform'; +import { isMacOS, isWindows } from '../../shared/platform'; import { WindowConfig } from '../config'; import type MenuBuilder from '../menu'; @@ -45,15 +45,27 @@ export function applyWindowVibrancy(mb: Menubar, enabled: boolean): void { /** * Apply the user's "keep window open when it loses focus" preference. * - * Implemented by toggling the window's `alwaysOnTop` flag, which the - * `menubar` library checks to short-circuit its blur-driven hide. The - * value is also remembered so the `devtools-closed` handler can restore - * it after DevTools temporarily forces it on. + * Click-away dismissal is independent of stacking: Windows popups must + * stay above the tray overflow even when they should hide on blur. */ export function applyKeepWindowOnBlur(mb: Menubar, value: boolean): void { keepWindowOnBlur = value; - if (mb.window && !mb.window.isDestroyed()) { - mb.window.setAlwaysOnTop(value); + applyWindowFocusBehavior(mb, value); +} + +function applyWindowFocusBehavior(mb: Menubar, keepOpen: boolean): void { + const win = mb.window; + if (!win || win.isDestroyed()) { + return; + } + + const shouldKeepOpen = keepOpen || win.webContents.isDevToolsOpened(); + mb.setOption('hideOnBlur', !shouldKeepOpen); + if (isWindows()) { + // The default floating level sits below the Windows tray overflow (#1048). + win.setAlwaysOnTop(true, 'pop-up-menu'); + } else { + win.setAlwaysOnTop(shouldKeepOpen); } } @@ -74,6 +86,8 @@ export function configureWindowEvents(mb: Menubar, menuBuilder: MenuBuilder): vo return; } + applyWindowFocusBehavior(mb, keepWindowOnBlur); + win.on('show', () => { menuBuilder.setWindowVisibility(true); // Re-apply vibrancy in case the window was rebuilt since it was last set. @@ -114,15 +128,14 @@ export function configureWindowEvents(mb: Menubar, menuBuilder: MenuBuilder): vo mb.window.setSize(800, 600); mb.window.center(); mb.window.resizable = true; - mb.window.setAlwaysOnTop(true); + applyWindowFocusBehavior(mb, true); }); /** * When DevTools is closed, restore the window to its original size and position it centered on the tray icon. * - * `devtools-opened` forces `alwaysOnTop` true for usability while - * debugging; restore it to the user's preference here so DevTools - * doesn't leave the flag stuck on. + * Restore click-away dismissal to the user's preference while retaining + * the Windows stacking level required to appear above the tray overflow. */ mb.window.webContents.on('devtools-closed', () => { if (!mb.window) { @@ -132,6 +145,6 @@ export function configureWindowEvents(mb: Menubar, menuBuilder: MenuBuilder): vo mb.window.setSize(WindowConfig.width!, WindowConfig.height!); mb.recenterOnTray(); mb.window.resizable = false; - mb.window.setAlwaysOnTop(keepWindowOnBlur); + applyWindowFocusBehavior(mb, keepWindowOnBlur); }); } From db4c287743084ceb24d0faca5810553ef3f69503 Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Sun, 6 Sep 2026 00:35:09 +0200 Subject: [PATCH 2/4] chore(deps): update electron-menubar to 10.3.0 --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index f095c47e6..9e884695a 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "dependencies": { "cn": "0.2.4", "electron-log": "5.4.4", - "electron-menubar": "10.2.1", + "electron-menubar": "10.3.0", "electron-updater": "6.8.9", "react": "19.2.8", "react-dom": "19.2.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1edd5ed2..0753202c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: 5.4.4 version: 5.4.4 electron-menubar: - specifier: 10.2.1 - version: 10.2.1(electron@44.1.1(supports-color@10.2.2)) + specifier: 10.3.0 + version: 10.3.0(electron@44.1.1(supports-color@10.2.2)) electron-updater: specifier: 6.8.9 version: 6.8.9(supports-color@10.2.2) @@ -2566,8 +2566,8 @@ packages: resolution: {integrity: sha512-istWgaXjBfURBSS8LWVW9C3jsc6+ac+tY1lXrQEOTp0lVj+a4OlO1Tmqb36GgnEUDv92DGC9VI1HNXwJinWpgA==} engines: {node: '>= 14'} - electron-menubar@10.2.1: - resolution: {integrity: sha512-JIpqGjVYUSloNJn/8hqotzFIRNmJ0jpGQJjXGs4GB/viy8cXIBlWj2G30w+SqJR7TKa+EBhXrMLSA73HdP/VgA==} + electron-menubar@10.3.0: + resolution: {integrity: sha512-t99wvadyY27DgJlwGWH1gInkV/Md5nDFfMuGS+7K6g6NhSQpIlpt59DWS1/ZeR2J+PArHbFer/nYgKCK5jd1cA==} peerDependencies: electron: 44.1.1 @@ -6868,7 +6868,7 @@ snapshots: electron-log@5.4.4: {} - electron-menubar@10.2.1(electron@44.1.1(supports-color@10.2.2)): + electron-menubar@10.3.0(electron@44.1.1(supports-color@10.2.2)): dependencies: electron: 44.1.1(supports-color@10.2.2) From 4319fa659543ea6946d14f3c5ab460d1eb37fd80 Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Sun, 6 Sep 2026 01:03:44 +0200 Subject: [PATCH 3/4] refactor: delegate popup focus behavior to menubar --- src/main/lifecycle/window.test.ts | 72 ++----------------------------- src/main/lifecycle/window.ts | 36 ++-------------- 2 files changed, 7 insertions(+), 101 deletions(-) diff --git a/src/main/lifecycle/window.test.ts b/src/main/lifecycle/window.test.ts index 89d93405b..8756f468e 100644 --- a/src/main/lifecycle/window.test.ts +++ b/src/main/lifecycle/window.test.ts @@ -154,79 +154,15 @@ describe('main/lifecycle/window.ts', () => { }); describe('applyKeepWindowOnBlur', () => { - it.each([false, true])('keeps Windows above the tray with keep-open set to %s', (keepOpen) => { - setPlatform('win32'); - + it.each([false, true])('forwards the keep-open preference %s to menubar', (keepOpen) => { applyKeepWindowOnBlur(menubar, keepOpen); - - expect(menubar.window?.setAlwaysOnTop).toHaveBeenCalledWith(true, 'pop-up-menu'); expect(menubar.setOption).toHaveBeenCalledWith('hideOnBlur', !keepOpen); }); - it('raises the Windows popup before its first tray click', () => { - setPlatform('win32'); - - configureWindowEvents(menubar, menuBuilder); - - expect(menubar.window?.setAlwaysOnTop).toHaveBeenCalledWith(true, 'pop-up-menu'); - expect(menubar.setOption).toHaveBeenCalledWith('hideOnBlur', true); - }); - - it('preserves Windows stacking and restores click-away after DevTools closes', () => { - setPlatform('win32'); - configureWindowEvents(menubar, menuBuilder); - - findWebContentsHandler(menubar, 'devtools-opened')?.(); - expect(menubar.setOption).toHaveBeenLastCalledWith('hideOnBlur', false); - expect(menubar.window?.setAlwaysOnTop).toHaveBeenLastCalledWith(true, 'pop-up-menu'); - - findWebContentsHandler(menubar, 'devtools-closed')?.(); - expect(menubar.setOption).toHaveBeenLastCalledWith('hideOnBlur', true); - expect(menubar.window?.setAlwaysOnTop).toHaveBeenLastCalledWith(true, 'pop-up-menu'); - }); - - it('keeps DevTools open when the keep-open setting changes', () => { - const webContents = menubar.window!.webContents; - vi.mocked(webContents.isDevToolsOpened).mockReturnValue(true); - - applyKeepWindowOnBlur(menubar, false); - - expect(menubar.setOption).toHaveBeenCalledWith('hideOnBlur', false); - expect(menubar.window?.setAlwaysOnTop).toHaveBeenCalledWith(true); - }); - - it('forwards the value to the underlying window', () => { - applyKeepWindowOnBlur(menubar, true); - - expect(menubar.window?.setAlwaysOnTop).toHaveBeenCalledWith(true); - }); - - it('skips the call when the window is destroyed', () => { - // oxlint-disable-next-line no-unsafe-optional-chaining -- window is guaranteed defined in this test - (menubar.window?.isDestroyed as ReturnType).mockReturnValue(true); - + it('remembers the preference through menubar before a window exists', () => { + Object.defineProperty(menubar, 'window', { value: undefined }); applyKeepWindowOnBlur(menubar, true); - - expect(menubar.window?.setAlwaysOnTop).not.toHaveBeenCalled(); - }); - - it('is restored after DevTools closes', () => { - configureWindowEvents(menubar, menuBuilder); - applyKeepWindowOnBlur(menubar, true); - // oxlint-disable-next-line no-unsafe-optional-chaining -- window is guaranteed defined in this test - (menubar.window?.setAlwaysOnTop as ReturnType).mockClear(); - - findWebContentsHandler(menubar, 'devtools-closed')?.(); - - expect(menubar.window?.setAlwaysOnTop).toHaveBeenCalledWith(true); - }); - - it('is cleared after DevTools closes when the user did not opt in', () => { - configureWindowEvents(menubar, menuBuilder); - - findWebContentsHandler(menubar, 'devtools-closed')?.(); - - expect(menubar.window?.setAlwaysOnTop).toHaveBeenCalledWith(false); + expect(menubar.setOption).toHaveBeenCalledWith('hideOnBlur', false); }); }); diff --git a/src/main/lifecycle/window.ts b/src/main/lifecycle/window.ts index ad25a708f..3d6e83e9d 100644 --- a/src/main/lifecycle/window.ts +++ b/src/main/lifecycle/window.ts @@ -1,13 +1,12 @@ import { app } from 'electron'; import type { Menubar } from 'electron-menubar'; -import { isMacOS, isWindows } from '../../shared/platform'; +import { isMacOS } from '../../shared/platform'; import { WindowConfig } from '../config'; import type MenuBuilder from '../menu'; let isQuitting = false; -let keepWindowOnBlur = false; let windowVibrancyEnabled = false; /** @@ -19,7 +18,6 @@ let windowVibrancyEnabled = false; */ export function __resetWindowLifecycleForTests(): void { isQuitting = false; - keepWindowOnBlur = false; windowVibrancyEnabled = false; } @@ -42,31 +40,8 @@ export function applyWindowVibrancy(mb: Menubar, enabled: boolean): void { mb.window.setBackgroundColor(enabled ? '#00000000' : '#ffffff'); } -/** - * Apply the user's "keep window open when it loses focus" preference. - * - * Click-away dismissal is independent of stacking: Windows popups must - * stay above the tray overflow even when they should hide on blur. - */ export function applyKeepWindowOnBlur(mb: Menubar, value: boolean): void { - keepWindowOnBlur = value; - applyWindowFocusBehavior(mb, value); -} - -function applyWindowFocusBehavior(mb: Menubar, keepOpen: boolean): void { - const win = mb.window; - if (!win || win.isDestroyed()) { - return; - } - - const shouldKeepOpen = keepOpen || win.webContents.isDevToolsOpened(); - mb.setOption('hideOnBlur', !shouldKeepOpen); - if (isWindows()) { - // The default floating level sits below the Windows tray overflow (#1048). - win.setAlwaysOnTop(true, 'pop-up-menu'); - } else { - win.setAlwaysOnTop(shouldKeepOpen); - } + mb.setOption('hideOnBlur', !value); } /** @@ -86,8 +61,6 @@ export function configureWindowEvents(mb: Menubar, menuBuilder: MenuBuilder): vo return; } - applyWindowFocusBehavior(mb, keepWindowOnBlur); - win.on('show', () => { menuBuilder.setWindowVisibility(true); // Re-apply vibrancy in case the window was rebuilt since it was last set. @@ -128,14 +101,12 @@ export function configureWindowEvents(mb: Menubar, menuBuilder: MenuBuilder): vo mb.window.setSize(800, 600); mb.window.center(); mb.window.resizable = true; - applyWindowFocusBehavior(mb, true); }); /** * When DevTools is closed, restore the window to its original size and position it centered on the tray icon. * - * Restore click-away dismissal to the user's preference while retaining - * the Windows stacking level required to appear above the tray overflow. + * Menubar restores focus behavior; Gitify restores its preferred layout. */ mb.window.webContents.on('devtools-closed', () => { if (!mb.window) { @@ -145,6 +116,5 @@ export function configureWindowEvents(mb: Menubar, menuBuilder: MenuBuilder): vo mb.window.setSize(WindowConfig.width!, WindowConfig.height!); mb.recenterOnTray(); mb.window.resizable = false; - applyWindowFocusBehavior(mb, keepWindowOnBlur); }); } From 94b268522d9deb20fbd5f168abc79fdb7aa925ec Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Sun, 6 Sep 2026 01:26:16 +0200 Subject: [PATCH 4/4] chore(deps): update electron-menubar to 11.0.0 --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 9e884695a..1057f8d52 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "dependencies": { "cn": "0.2.4", "electron-log": "5.4.4", - "electron-menubar": "10.3.0", + "electron-menubar": "11.0.0", "electron-updater": "6.8.9", "react": "19.2.8", "react-dom": "19.2.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0753202c6..a59f16043 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: 5.4.4 version: 5.4.4 electron-menubar: - specifier: 10.3.0 - version: 10.3.0(electron@44.1.1(supports-color@10.2.2)) + specifier: 11.0.0 + version: 11.0.0(electron@44.1.1(supports-color@10.2.2)) electron-updater: specifier: 6.8.9 version: 6.8.9(supports-color@10.2.2) @@ -2566,8 +2566,8 @@ packages: resolution: {integrity: sha512-istWgaXjBfURBSS8LWVW9C3jsc6+ac+tY1lXrQEOTp0lVj+a4OlO1Tmqb36GgnEUDv92DGC9VI1HNXwJinWpgA==} engines: {node: '>= 14'} - electron-menubar@10.3.0: - resolution: {integrity: sha512-t99wvadyY27DgJlwGWH1gInkV/Md5nDFfMuGS+7K6g6NhSQpIlpt59DWS1/ZeR2J+PArHbFer/nYgKCK5jd1cA==} + electron-menubar@11.0.0: + resolution: {integrity: sha512-KepfKfvbP5FKVTAno3IqBmNJwFxjSToYaZYjB3Cu3J3eae+L2Hc3JBlNIaeYJyzeO5KL73s+zYveuxF8AJ87Yw==} peerDependencies: electron: 44.1.1 @@ -6868,7 +6868,7 @@ snapshots: electron-log@5.4.4: {} - electron-menubar@10.3.0(electron@44.1.1(supports-color@10.2.2)): + electron-menubar@11.0.0(electron@44.1.1(supports-color@10.2.2)): dependencies: electron: 44.1.1(supports-color@10.2.2)