mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
fix(window): restore native fullscreen after a restart (#40540)
Persist windowFullScreen on enter/leave-full-screen (frozen once the window starts closing) and re-enter fullscreen on reveal after the saved maximize, skipping background launches. Ignore the spurious unmaximize macOS emits while a launch-maximized window enters fullscreen so windowMaximized and windowBounds keep the pre-fullscreen state. Fixes #40510 Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
This commit is contained in:
@@ -11,6 +11,7 @@ type MainOwnedUIState =
|
||||
| '_explorerDisplayRootMigrated'
|
||||
| 'trayMinimizeNoticeShown'
|
||||
| 'dashboardPopoutBounds'
|
||||
| 'windowFullScreen'
|
||||
| '_expandedWorktreeCardPropertiesDefaulted'
|
||||
| '_jiraIssueWorktreeCardPropertyDefaulted'
|
||||
| '_hostWorktreeCardPropertyDefaulted'
|
||||
|
||||
@@ -88,6 +88,7 @@ export function createMainWindow(
|
||||
)
|
||||
}
|
||||
const savedMaximized = store?.getUI().windowMaximized ?? false
|
||||
const savedFullScreen = store?.getUI().windowFullScreen ?? false
|
||||
// Fill the primary work area on first launch; later launches honor saved bounds.
|
||||
const defaultBounds = (() => {
|
||||
try {
|
||||
@@ -203,6 +204,7 @@ export function createMainWindow(
|
||||
mainWindow,
|
||||
revealOnDidFinishLoad: opts?.revealOnDidFinishLoad === true,
|
||||
savedMaximized,
|
||||
savedFullScreen,
|
||||
store
|
||||
})
|
||||
installMainWindowWebviewSecurity(mainWindow)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
|
||||
const { appMock } = await vi.hoisted(async () => {
|
||||
const { appMock, launchPolicy } = await vi.hoisted(async () => {
|
||||
const { EventEmitter } = await import('node:events')
|
||||
return { appMock: new EventEmitter() }
|
||||
return { appMock: new EventEmitter(), launchPolicy: { background: false, windowless: true } }
|
||||
})
|
||||
vi.mock('electron', () => ({ app: appMock }))
|
||||
vi.mock('./foreground-activation-policy', () => ({
|
||||
isWindowlessLaunch: () => true,
|
||||
isBackgroundLaunch: () => launchPolicy.background,
|
||||
isWindowlessLaunch: () => launchPolicy.windowless,
|
||||
showWindowWithoutStealingFocus: vi.fn()
|
||||
}))
|
||||
vi.mock('./main-window-visual-lifecycle', () => ({
|
||||
@@ -21,6 +22,8 @@ import { installMainWindowStateLifecycle } from './main-window-state-lifecycle'
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
appMock.removeAllListeners()
|
||||
launchPolicy.background = false
|
||||
launchPolicy.windowless = true
|
||||
})
|
||||
afterEach(() => vi.useRealTimers())
|
||||
|
||||
@@ -41,6 +44,7 @@ it('continues saving bounds after an updater quit veto and freezes them on allow
|
||||
mainWindow: mainWindow as never,
|
||||
revealOnDidFinishLoad: false,
|
||||
savedMaximized: false,
|
||||
savedFullScreen: false,
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Bounds persistence reads only updateUI from this store fixture.
|
||||
store: { updateUI } as never
|
||||
})
|
||||
@@ -62,3 +66,85 @@ it('continues saving bounds after an updater quit veto and freezes them on allow
|
||||
lifecycle.clearInitialRevealFallbackTimer()
|
||||
lifecycle.dispose()
|
||||
})
|
||||
|
||||
function installFullScreenFixture(): {
|
||||
lifecycle: ReturnType<typeof installMainWindowStateLifecycle>
|
||||
mainWindow: EventEmitter & { setFullScreen: (flag: boolean) => void }
|
||||
windowState: { maximized: boolean; fullScreen: boolean; maximizedUnderFullScreen: boolean }
|
||||
persisted: Record<string, unknown>
|
||||
} {
|
||||
launchPolicy.windowless = false
|
||||
const windowState = { maximized: false, fullScreen: false, maximizedUnderFullScreen: false }
|
||||
const mainWindow = Object.assign(new EventEmitter(), {
|
||||
webContents: Object.assign(new EventEmitter(), { send: vi.fn(), setZoomLevel: vi.fn() }),
|
||||
isDestroyed: () => false,
|
||||
isFullScreen: () => windowState.fullScreen,
|
||||
getBounds: () => ({ x: 0, y: 0, width: 2560, height: 1440 }),
|
||||
maximize: () => {
|
||||
windowState.maximized = true
|
||||
},
|
||||
setFullScreen: (flag: boolean) => {
|
||||
if (flag) {
|
||||
windowState.maximizedUnderFullScreen = windowState.maximized
|
||||
}
|
||||
windowState.fullScreen = flag
|
||||
}
|
||||
})
|
||||
const persisted: Record<string, unknown> = {}
|
||||
const lifecycle = installMainWindowStateLifecycle({
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This fixture implements the window members read by reveal and fullscreen persistence.
|
||||
mainWindow: mainWindow as never,
|
||||
revealOnDidFinishLoad: false,
|
||||
savedMaximized: true,
|
||||
savedFullScreen: true,
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Fullscreen persistence reads only updateUI from this store fixture.
|
||||
store: {
|
||||
updateUI: (patch: Record<string, unknown>) => Object.assign(persisted, patch)
|
||||
} as never
|
||||
})
|
||||
return { lifecycle, mainWindow, windowState, persisted }
|
||||
}
|
||||
|
||||
it('restores native fullscreen over the maximized frame and keeps that frame while fullscreen', () => {
|
||||
const { lifecycle, mainWindow, windowState, persisted } = installFullScreenFixture()
|
||||
const leaveFullScreen = (): void => {
|
||||
mainWindow.setFullScreen(false)
|
||||
mainWindow.emit('leave-full-screen')
|
||||
}
|
||||
mainWindow.emit('ready-to-show')
|
||||
expect(windowState).toEqual({ maximized: true, fullScreen: true, maximizedUnderFullScreen: true })
|
||||
// macOS emits unmaximize mid-transition, after the fullscreen style mask is set.
|
||||
mainWindow.emit('unmaximize')
|
||||
mainWindow.emit('enter-full-screen')
|
||||
expect(persisted).toEqual({ windowFullScreen: true })
|
||||
|
||||
leaveFullScreen()
|
||||
expect(persisted).toEqual({ windowFullScreen: false })
|
||||
mainWindow.emit('unmaximize')
|
||||
expect(persisted).toEqual({
|
||||
windowFullScreen: false,
|
||||
windowMaximized: false,
|
||||
windowBounds: { x: 0, y: 0, width: 2560, height: 1440 }
|
||||
})
|
||||
|
||||
mainWindow.setFullScreen(true)
|
||||
mainWindow.emit('enter-full-screen')
|
||||
appMock.emit('before-quit', { defaultPrevented: false })
|
||||
leaveFullScreen()
|
||||
expect(persisted).toMatchObject({ windowFullScreen: true })
|
||||
lifecycle.clearInitialRevealFallbackTimer()
|
||||
lifecycle.dispose()
|
||||
})
|
||||
|
||||
it('does not restore native fullscreen for background launches', () => {
|
||||
launchPolicy.background = true
|
||||
const { lifecycle, mainWindow, windowState } = installFullScreenFixture()
|
||||
mainWindow.emit('ready-to-show')
|
||||
expect(windowState).toEqual({
|
||||
maximized: true,
|
||||
fullScreen: false,
|
||||
maximizedUnderFullScreen: false
|
||||
})
|
||||
lifecycle.clearInitialRevealFallbackTimer()
|
||||
lifecycle.dispose()
|
||||
})
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { app, type BrowserWindow, type Event } from 'electron'
|
||||
import type { Store } from '../persistence/loading-store/store'
|
||||
import { uiZoomFactorFromLevel } from '../../shared/ui-zoom-level'
|
||||
import { isWindowlessLaunch, showWindowWithoutStealingFocus } from './foreground-activation-policy'
|
||||
import {
|
||||
isBackgroundLaunch,
|
||||
isWindowlessLaunch,
|
||||
showWindowWithoutStealingFocus
|
||||
} from './foreground-activation-policy'
|
||||
import { MIN_HEIGHT, MIN_WIDTH, syncTrafficLightPosition } from './main-window-visual-lifecycle'
|
||||
|
||||
export type MainWindowStateLifecycle = {
|
||||
@@ -16,9 +20,10 @@ export function installMainWindowStateLifecycle(args: {
|
||||
mainWindow: BrowserWindow
|
||||
revealOnDidFinishLoad: boolean
|
||||
savedMaximized: boolean
|
||||
savedFullScreen: boolean
|
||||
store: Store | null
|
||||
}): MainWindowStateLifecycle {
|
||||
const { mainWindow, revealOnDidFinishLoad, savedMaximized, store } = args
|
||||
const { mainWindow, revealOnDidFinishLoad, savedMaximized, savedFullScreen, store } = args
|
||||
mainWindow.webContents.on('dom-ready', () => {
|
||||
const level = store?.getUI().uiZoomLevel ?? 0
|
||||
mainWindow.webContents.setZoomLevel(level)
|
||||
@@ -66,6 +71,10 @@ export function installMainWindowStateLifecycle(args: {
|
||||
mainWindow.maximize()
|
||||
}
|
||||
showWindowWithoutStealingFocus(mainWindow)
|
||||
// Fullscreen needs a shown window; after maximize so leaving it returns there. Background launches must not take a Space.
|
||||
if (savedFullScreen && !isBackgroundLaunch()) {
|
||||
mainWindow.setFullScreen(true)
|
||||
}
|
||||
}
|
||||
mainWindow.on('ready-to-show', revealInitialWindow)
|
||||
if (revealOnDidFinishLoad === true) {
|
||||
@@ -129,6 +138,10 @@ export function installMainWindowStateLifecycle(args: {
|
||||
return
|
||||
}
|
||||
mainWindow.webContents.send('window:maximize-changed', false)
|
||||
// macOS emits unmaximize when a launch-maximized window enters fullscreen; keep the pre-fullscreen state.
|
||||
if (mainWindow.isFullScreen()) {
|
||||
return
|
||||
}
|
||||
const bounds = mainWindow.getBounds()
|
||||
// Teardown unmaximize can emit minimum bounds; apply the same persistence guard.
|
||||
if (bounds.width <= MIN_WIDTH || bounds.height <= MIN_HEIGHT) {
|
||||
@@ -140,10 +153,16 @@ export function installMainWindowStateLifecycle(args: {
|
||||
})
|
||||
|
||||
mainWindow.on('enter-full-screen', () => {
|
||||
if (!windowClosing) {
|
||||
store?.updateUI({ windowFullScreen: true })
|
||||
}
|
||||
mainWindow.webContents.send('window:fullscreen-changed', true)
|
||||
})
|
||||
|
||||
mainWindow.on('leave-full-screen', () => {
|
||||
if (!windowClosing) {
|
||||
store?.updateUI({ windowFullScreen: false })
|
||||
}
|
||||
mainWindow.webContents.send('window:fullscreen-changed', false)
|
||||
})
|
||||
|
||||
|
||||
@@ -223,6 +223,8 @@ export type PersistedUIState = {
|
||||
windowBounds?: { x: number; y: number; width: number; height: number } | null
|
||||
/** Whether the window was maximized when it was last closed. */
|
||||
windowMaximized?: boolean
|
||||
/** Whether the window was in native fullscreen when it was last closed. */
|
||||
windowFullScreen?: boolean
|
||||
/** Dashboard popout bounds, independent of main-window bounds. */
|
||||
dashboardPopoutBounds?: { x: number; y: number; width: number; height: number } | null
|
||||
/** Migrate legacy recent→smart once so later activity-based recent selections survive. */
|
||||
|
||||
@@ -156,6 +156,7 @@ export const UI_STATE_GLOBAL_FIELDS = [
|
||||
'browserDevToolsDock',
|
||||
'windowBounds',
|
||||
'windowMaximized',
|
||||
'windowFullScreen',
|
||||
'dashboardPopoutBounds',
|
||||
'starNagBaselineAgents',
|
||||
'starNagAppVersion',
|
||||
|
||||
Reference in New Issue
Block a user