diff --git a/packages/compass-preferences-model/src/preferences-schema.tsx b/packages/compass-preferences-model/src/preferences-schema.tsx index 12199f292db..d6801b0c17f 100644 --- a/packages/compass-preferences-model/src/preferences-schema.tsx +++ b/packages/compass-preferences-model/src/preferences-schema.tsx @@ -126,6 +126,14 @@ export type InternalUserPreferences = { enableConnectInNewWindow: boolean; showEndOfLifeConnectionModal: boolean; zoomLevel?: number; + windowBounds?: { + x?: number; + y?: number; + width?: number; + height?: number; + isMaximized?: boolean; + isFullScreen?: boolean; + }; }; // UserPreferences contains all preferences stored to disk. @@ -471,6 +479,26 @@ export const storedUserPreferencesProps: Required<{ validator: z.number().optional(), type: 'number', }, + /** + * Window bounds for restoring window size and position. + */ + windowBounds: { + ui: false, + cli: false, + global: false, + description: null, + validator: z + .object({ + x: z.number().optional(), + y: z.number().optional(), + width: z.number().optional(), + height: z.number().optional(), + isMaximized: z.boolean().optional(), + isFullScreen: z.boolean().optional(), + }) + .optional(), + type: 'object', + }, /** * Enable/disable the AI services. This is currently set * in the atlas-service initialization where we make a request to the diff --git a/packages/compass/src/main/window-manager.ts b/packages/compass/src/main/window-manager.ts index fb74160e6cd..93cf8bc93ba 100644 --- a/packages/compass/src/main/window-manager.ts +++ b/packages/compass/src/main/window-manager.ts @@ -14,7 +14,7 @@ import type { import { app as electronApp, shell, BrowserWindow } from 'electron'; import { enable } from '@electron/remote/main'; -import { createLogger } from '@mongodb-js/compass-logging'; +import { createLogger, mongoLogId } from '@mongodb-js/compass-logging'; import COMPASS_ICON from './icon'; import type { CompassApplication } from './application'; import { @@ -23,7 +23,7 @@ import { registerConnectionIdForBrowserWindow, } from './auto-connect'; -const { debug } = createLogger('COMPASS-WINDOW-MANAGER'); +const { debug, log } = createLogger('COMPASS-WINDOW-MANAGER'); const earlyOpenUrls: string[] = []; function earlyOpenUrlListener( @@ -68,6 +68,7 @@ const DEFAULT_HEIGHT = (() => { // change significantly at widths of 1024 and less. const MIN_WIDTH = process.env.COMPASS_MIN_WIDTH ?? 1025; const MIN_HEIGHT = process.env.COMPASS_MIN_HEIGHT ?? 640; +const SAVE_DEBOUNCE_DELAY = 500; // 500ms delay for save operations /** * The app's HTML shell which is the output of `./src/index.html` @@ -77,11 +78,73 @@ const DEFAULT_URL = process.env.COMPASS_INDEX_RENDERER_URL || pathToFileURL(path.join(__dirname, 'index.html')).toString(); -async function showWindowWhenReady(bw: BrowserWindow) { +async function showWindowWhenReady( + bw: BrowserWindow, + isMaximized?: boolean, + isFullScreen?: boolean +) { await once(bw, 'ready-to-show'); + if (isMaximized) { + // win.maximize() maximizes the window. + // This will also show (but not focus) the window if it isn't being displayed already. + bw.maximize(); + bw.focus(); + return; + } + + if (isFullScreen) { + bw.setFullScreen(true); + } + bw.show(); } +/** + * Save window bounds to preferences + */ +async function saveWindowBounds( + window: BrowserWindow, + compassApp: typeof CompassApplication +) { + try { + const bounds = window.getBounds(); + const isMaximized = window.isMaximized(); + const isFullScreen = window.isFullScreen(); + + await compassApp.preferences.savePreferences({ + windowBounds: { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + isMaximized, + isFullScreen, + }, + }); + } catch (err) { + log.warn( + mongoLogId(1_001_000_377), + 'Window Manager', + 'Failed to save window bounds', + { message: (err as Error).message } + ); + } +} + +/** + * Get saved window bounds from preferences + */ +function getSavedWindowBounds(compassApp: typeof CompassApplication) { + const windowBounds = + compassApp.preferences.getPreferences().windowBounds ?? {}; + const { width, height, ...rest } = windowBounds; + return { + ...rest, + width: width ?? DEFAULT_WIDTH, + height: height ?? DEFAULT_HEIGHT, + }; +} + /** * Call me instead of using `new BrowserWindow()` directly because i'll: * @@ -109,9 +172,11 @@ function showConnectWindow( } > = {} ): BrowserWindow { + // Get saved window bounds + const { isMaximized, isFullScreen, ...bounds } = + getSavedWindowBounds(compassApp); const windowOpts = { - width: Number(DEFAULT_WIDTH), - height: Number(DEFAULT_HEIGHT), + ...bounds, minWidth: Number(MIN_WIDTH), minHeight: Number(MIN_HEIGHT), /** @@ -156,8 +221,35 @@ function showConnectWindow( compassApp.emit('new-window', window); + // Set up window state persistence + let saveTimeoutId: NodeJS.Timeout | null = null; + const debouncedSaveWindowBounds = () => { + if (saveTimeoutId) { + clearTimeout(saveTimeoutId); + } + saveTimeoutId = setTimeout(() => { + if (window && !window.isDestroyed()) { + void saveWindowBounds(window, compassApp); + } + saveTimeoutId = null; + }, SAVE_DEBOUNCE_DELAY); // Debounce to avoid too frequent saves + }; + + // Save window bounds when moved or resized + window.on('move', debouncedSaveWindowBounds); + window.on('moved', debouncedSaveWindowBounds); + window.on('resize', debouncedSaveWindowBounds); + window.on('resized', debouncedSaveWindowBounds); + window.on('maximize', debouncedSaveWindowBounds); + window.on('unmaximize', debouncedSaveWindowBounds); + window.on('enter-full-screen', debouncedSaveWindowBounds); + window.on('leave-full-screen', debouncedSaveWindowBounds); + const onWindowClosed = () => { debug('Window closed. Dereferencing.'); + if (saveTimeoutId) { + clearTimeout(saveTimeoutId); + } window = null; void unsubscribeProxyListenerPromise.then((unsubscribe) => unsubscribe()); }; @@ -166,7 +258,7 @@ function showConnectWindow( debug(`Loading page ${rendererUrl} in main window`); - void showWindowWhenReady(window); + void showWindowWhenReady(window, isMaximized, isFullScreen); void window.loadURL(rendererUrl); @@ -243,6 +335,8 @@ class CompassWindowManager { if (first) { debug('sending `app:quit` msg'); first.webContents.send('app:quit'); + // Save window bounds before quitting + void saveWindowBounds(first, compassApp); } });