diff --git a/src/main.ts b/src/main.ts index 1af3c941e00..fc3b6ad6a35 100644 --- a/src/main.ts +++ b/src/main.ts @@ -249,7 +249,10 @@ function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { 'log-level', // Use an in-memory storage for secrets - 'use-inmemory-secretstorage' + 'use-inmemory-secretstorage', + + // Enables display tracking to restore maximized windows under RDP: https://github.com/electron/electron/issues/47016 + 'enable-rdp-display-tracking' ]; // Read argv config @@ -307,6 +310,12 @@ function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { process.argv.push('--use-inmemory-secretstorage'); } break; + + case 'enable-rdp-display-tracking': + if (argvValue) { + process.argv.push('--enable-rdp-display-tracking'); + } + break; } } }); @@ -359,6 +368,7 @@ interface IArgvConfig { readonly 'log-level'?: string | string[]; readonly 'disable-chromium-sandbox'?: boolean; readonly 'use-inmemory-secretstorage'?: boolean; + readonly 'enable-rdp-display-tracking'?: boolean; } function readArgvConfigSync(): IArgvConfig { diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index e4522f6492e..ead93c8ec0b 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -124,6 +124,7 @@ export interface NativeParsedArgs { 'enable-coi'?: boolean; 'unresponsive-sample-interval'?: string; 'unresponsive-sample-period'?: string; + 'enable-rdp-display-tracking'?: boolean; // chromium command line args: https://electronjs.org/docs/all#supported-chrome-command-line-switches 'no-proxy-server'?: boolean; diff --git a/src/vs/platform/environment/electron-main/environmentMainService.ts b/src/vs/platform/environment/electron-main/environmentMainService.ts index 6fe1e58d11a..08c5abbc8f8 100644 --- a/src/vs/platform/environment/electron-main/environmentMainService.ts +++ b/src/vs/platform/environment/electron-main/environmentMainService.ts @@ -33,6 +33,9 @@ export interface IEnvironmentMainService extends INativeEnvironmentService { // --- config readonly disableUpdates: boolean; + // TODO@deepak1556 TODO@bpasero temporary until a real fix lands upstream + readonly enableRDPDisplayTracking: boolean; + unsetSnapExportedVariables(): void; restoreSnapExportedVariables(): void; } @@ -56,6 +59,9 @@ export class EnvironmentMainService extends NativeEnvironmentService implements @memoize get crossOriginIsolated(): boolean { return !!this.args['enable-coi']; } + @memoize + get enableRDPDisplayTracking(): boolean { return !!this.args['enable-rdp-display-tracking']; } + @memoize get codeCachePath(): string | undefined { return process.env['VSCODE_CODE_CACHE_PATH'] || undefined; } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index e3ac83e60aa..d41a2e9fbd1 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -185,6 +185,7 @@ export const OPTIONS: OptionDescriptions> = { 'enable-coi': { type: 'boolean' }, 'unresponsive-sample-interval': { type: 'string' }, 'unresponsive-sample-period': { type: 'string' }, + 'enable-rdp-display-tracking': { type: 'boolean' }, // chromium flags 'no-proxy-server': { type: 'boolean' }, diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 4a8fa401901..1e031a1f571 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import electron, { BrowserWindowConstructorOptions } from 'electron'; +import electron, { BrowserWindowConstructorOptions, Display, screen } from 'electron'; import { DeferredPromise, RunOnceScheduler, timeout, Delayer } from '../../../base/common/async.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { toErrorMessage } from '../../../base/common/errorMessage.js'; @@ -116,14 +116,34 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { protected _lastFocusTime = Date.now(); // window is shown on creation so take current time get lastFocusTime(): number { return this._lastFocusTime; } + private maximizedWindowState: IWindowState | undefined; + protected _win: electron.BrowserWindow | null = null; get win() { return this._win; } protected setWin(win: electron.BrowserWindow, options?: BrowserWindowConstructorOptions): void { this._win = win; // Window Events - this._register(Event.fromNodeEventEmitter(win, 'maximize')(() => this._onDidMaximize.fire())); - this._register(Event.fromNodeEventEmitter(win, 'unmaximize')(() => this._onDidUnmaximize.fire())); + this._register(Event.fromNodeEventEmitter(win, 'maximize')(() => { + if (isWindows && this.environmentMainService.enableRDPDisplayTracking && this._win) { + const [x, y] = this._win.getPosition(); + const [width, height] = this._win.getSize(); + + this.maximizedWindowState = { mode: WindowMode.Maximized, width, height, x, y }; + this.logService.debug(`Saved maximized window ${this.id} display state:`, this.maximizedWindowState); + } + + this._onDidMaximize.fire(); + })); + this._register(Event.fromNodeEventEmitter(win, 'unmaximize')(() => { + if (isWindows && this.environmentMainService.enableRDPDisplayTracking && this.maximizedWindowState) { + this.maximizedWindowState = undefined; + + this.logService.debug(`Cleared maximized window ${this.id} state`); + } + + this._onDidUnmaximize.fire(); + })); this._register(Event.fromNodeEventEmitter(win, 'closed')(() => { this._onDidClose.fire(); @@ -198,6 +218,24 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { this.joinNativeFullScreenTransition?.complete(true); })); } + + if (isWindows && this.environmentMainService.enableRDPDisplayTracking) { + // Handles the display-added event on Windows RDP multi-monitor scenarios. + // This helps restore maximized windows to their correct monitor after RDP reconnection. + // Refs https://github.com/electron/electron/issues/47016 + this._register(Event.fromNodeEventEmitter(screen, 'display-added', (event: Electron.Event, display: Display) => ({ event, display }))((e) => { + this.onDisplayAdded(e.display); + })); + } + } + + private onDisplayAdded(display: Display): void { + const state = this.maximizedWindowState; + if (state && this._win && WindowStateValidator.validateWindowStateOnDisplay(state, display)) { + this.logService.debug(`Setting maximized window ${this.id} bounds to match newly added display`, state); + + this._win.setBounds(state); + } } constructor( diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 42be1e253a5..8fb315a001d 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import electron from 'electron'; +import electron, { Display, Rectangle } from 'electron'; import { Color } from '../../../base/common/color.js'; import { Event } from '../../../base/common/event.js'; import { join } from '../../../base/common/path.js'; @@ -350,14 +350,7 @@ export namespace WindowStateValidator { logService.error('window#validateWindowState: error finding display for window state', error); } - if ( - display && // we have a display matching the desired bounds - displayWorkingArea && // we have valid working area bounds - state.x + state.width > displayWorkingArea.x && // prevent window from falling out of the screen to the left - state.y + state.height > displayWorkingArea.y && // prevent window from falling out of the screen to the top - state.x < displayWorkingArea.x + displayWorkingArea.width && // prevent window from falling out of the screen to the right - state.y < displayWorkingArea.y + displayWorkingArea.height // prevent window from falling out of the screen to the bottom - ) { + if (display && validateWindowStateOnDisplay(state, display)) { return state; } @@ -366,6 +359,27 @@ export namespace WindowStateValidator { return undefined; } + export function validateWindowStateOnDisplay(state: IWindowState, display: Display): state is Rectangle { + if ( + typeof state.x !== 'number' || + typeof state.y !== 'number' || + typeof state.width !== 'number' || + typeof state.height !== 'number' || + state.width <= 0 || state.height <= 0 + ) { + return false; + } + + const displayWorkingArea = getWorkingArea(display); + return Boolean( + displayWorkingArea && // we have valid working area bounds + state.x + state.width > displayWorkingArea.x && // prevent window from falling out of the screen to the left + state.y + state.height > displayWorkingArea.y && // prevent window from falling out of the screen to the top + state.x < displayWorkingArea.x + displayWorkingArea.width && // prevent window from falling out of the screen to the right + state.y < displayWorkingArea.y + displayWorkingArea.height // prevent window from falling out of the screen to the bottom + ); + } + function getWorkingArea(display: electron.Display): electron.Rectangle | undefined { // Prefer the working area of the display to account for taskbars on the diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index e4b863b0b4a..975fd456b23 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -426,6 +426,12 @@ import { registerWorkbenchContribution2, WorkbenchPhase } from '../common/contri description: localize('argv.passwordStore', "Configures the backend used to store secrets on Linux. This argument is ignored on Windows & macOS.") }; } + if (isWindows) { + schema.properties!['enable-rdp-display-tracking'] = { + type: 'boolean', + description: localize('argv.enableRDPDisplayTracking', "Ensures that maximized windows gets restored to correct display during RDP reconnection.") + }; + } jsonRegistry.registerSchema(argvDefinitionFileSchemaId, schema); })();