diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index aaf950b4c1f..4a6be70c633 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -749,9 +749,14 @@ export function isActiveDocument(element: Element): boolean { /** * Returns the active document across all child windows. - * Use this instead of `document` when reacting to dom events to handle multiple windows. + * Use this instead of `document` when reacting to dom + * events to handle multiple windows. */ export function getActiveDocument(): Document { + if (getWindowsCount() <= 1) { + return document; + } + const documents = Array.from(getWindows()).map(window => window.document); return documents.find(doc => doc.hasFocus()) ?? document; } @@ -797,9 +802,6 @@ export function createStyleSheet(container: HTMLElement = document.head, beforeA // With as container, the stylesheet becomes global and is tracked // to support auxiliary windows to clone the stylesheet. if (container === document.head) { - const clonedGlobalStylesheets = new Set(); - globalStylesheets.set(style, clonedGlobalStylesheets); - for (const targetWindow of getWindows()) { if (targetWindow === window) { continue; // main window is already tracked @@ -845,13 +847,18 @@ function cloneGlobalStyleSheet(globalStylesheet: HTMLStyleElement, targetWindow: }); observer.observe(globalStylesheet, { childList: true }); - globalStylesheets.get(globalStylesheet)?.add(clone); + let clonedGlobalStylesheets = globalStylesheets.get(globalStylesheet); + if (!clonedGlobalStylesheets) { + clonedGlobalStylesheets = new Set(); + globalStylesheets.set(globalStylesheet, clonedGlobalStylesheets); + } + clonedGlobalStylesheets.add(clone); return toDisposable(() => { observer.disconnect(); targetWindow.document.head.removeChild(clone); - globalStylesheets.get(globalStylesheet)?.delete(clone); + clonedGlobalStylesheets?.delete(clone); }); } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 1554dfade7b..9eb840c0707 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -5,7 +5,7 @@ import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { EventType, addDisposableListener, getClientArea, Dimension, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, focusWindow, isActiveDocument } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, getClientArea, Dimension, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, focusWindow, isActiveDocument, getWindow } from 'vs/base/browser/dom'; import { onDidChangeFullscreen, isFullscreen, isWCOEnabled } from 'vs/base/browser/browser'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { isWindows, isLinux, isMacintosh, isWeb, isNative, isIOS } from 'vs/base/common/platform'; @@ -47,11 +47,12 @@ import { IBannerService } from 'vs/workbench/services/banner/browser/bannerServi import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { AuxiliaryBarPart } from 'vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; +import { IAuxiliaryWindowService, isAuxiliaryWindow } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; //#region Layout Implementation interface ILayoutRuntimeState { + activeContainer: 'main' | number /* window ID */; fullscreen: boolean; maximized: boolean; hasFocus: boolean; @@ -451,10 +452,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private onWindowFocusChanged(hasFocus: boolean): void { if (hasFocus) { - // This is a bit simplified: we assume that the active container - // has changed when receiving focus, but we might end up with - // the same active container as before... - this._onDidChangeActiveContainer.fire(); + const activeContainerId = this.getActiveContainerId(); + if (this.state.runtime.activeContainer !== activeContainerId) { + this.state.runtime.activeContainer = activeContainerId; + this._onDidChangeActiveContainer.fire(); + } } if (this.state.runtime.hasFocus !== hasFocus) { @@ -463,6 +465,18 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } } + private getActiveContainerId(): 'main' | number { + const activeContainer = this.activeContainer; + if (activeContainer !== this.container) { + const containerWindow = getWindow(activeContainer); + if (isAuxiliaryWindow(containerWindow)) { + return containerWindow.vscodeWindowId; + } + } + + return 'main'; + } + private doUpdateLayoutConfiguration(skipLayout?: boolean): void { // Menubar visibility @@ -601,6 +615,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Layout Runtime State const layoutRuntimeState: ILayoutRuntimeState = { + activeContainer: this.getActiveContainerId(), fullscreen: isFullscreen(), hasFocus: this.hostService.hasFocus, maximized: false, diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 30219f5b97d..963596b3eec 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -680,12 +680,12 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Close all inactive editors first to prevent UI flicker for (const inactiveEditor of inactiveEditors) { - this.doCloseEditor(inactiveEditor, false); + this.doCloseEditor(inactiveEditor, true); } // Close active one last if (activeEditor) { - this.doCloseEditor(activeEditor, false); + this.doCloseEditor(activeEditor, true); } } @@ -1108,8 +1108,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Without an editor pane, recover by closing the active editor // (if the input is still the active one) if (!pane && this.activeEditor === editor) { - const focusNext = !options || !options.preserveFocus; - this.doCloseEditor(editor, focusNext, { fromError: true }); + this.doCloseEditor(editor, options?.preserveFocus, { fromError: true }); } return pane; @@ -1283,7 +1282,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // ...and a close afterwards (unless we copy) if (!keepCopy) { - this.doCloseEditor(editor, false /* do not focus next one behind if any */, { ...internalOptions, context: EditorCloseContext.MOVE }); + this.doCloseEditor(editor, true /* do not focus next one behind if any */, { ...internalOptions, context: EditorCloseContext.MOVE }); } } @@ -1348,12 +1347,12 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } // Do close - this.doCloseEditor(editor, options?.preserveFocus ? false : undefined, internalOptions); + this.doCloseEditor(editor, options?.preserveFocus, internalOptions); return true; } - private doCloseEditor(editor: EditorInput, focusNext = (this.groupsView.activeGroup === this), internalOptions?: IInternalEditorCloseOptions): void { + private doCloseEditor(editor: EditorInput, preserveFocus = (this.groupsView.activeGroup !== this), internalOptions?: IInternalEditorCloseOptions): void { // Forward to title control unless skipped via internal options if (!internalOptions?.skipTitleUpdate) { @@ -1362,7 +1361,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Closing the active editor of the group is a bit more work if (this.model.isActive(editor)) { - this.doCloseActiveEditor(focusNext, internalOptions); + this.doCloseActiveEditor(preserveFocus, internalOptions); } // Closing inactive editor is just a model update @@ -1376,9 +1375,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } } - private doCloseActiveEditor(focusNext = (this.groupsView.activeGroup === this), internalOptions?: IInternalEditorCloseOptions): void { + private doCloseActiveEditor(preserveFocus = (this.groupsView.activeGroup !== this), internalOptions?: IInternalEditorCloseOptions): void { const editorToClose = this.activeEditor; - const restoreFocus = this.shouldRestoreFocus(this.element); + const restoreFocus = !preserveFocus && this.shouldRestoreFocus(this.element); // Optimization: if we are about to close the last editor in this group and settings // are configured to close the group since it will be empty, we first set the last @@ -1408,8 +1407,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Open next active if there are more to show const nextActiveEditor = this.model.activeEditor; if (nextActiveEditor) { - const preserveFocus = !focusNext; - let activation: EditorActivation | undefined = undefined; if (preserveFocus && this.groupsView.activeGroup !== this) { // If we are opening the next editor in an inactive group @@ -1723,7 +1720,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Close active editor last if contained in editors list to close if (closeActiveEditor) { - this.doCloseActiveEditor(options?.preserveFocus ? false : undefined); + this.doCloseActiveEditor(options?.preserveFocus); } // Forward to title control @@ -1827,7 +1824,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (!editor.matches(replacement)) { let closed = false; if (forceReplaceDirty) { - this.doCloseEditor(editor, false, { context: EditorCloseContext.REPLACE }); + this.doCloseEditor(editor, true, { context: EditorCloseContext.REPLACE }); closed = true; } else { closed = await this.doCloseEditorWithConfirmationHandling(editor, { preserveFocus: true }, { context: EditorCloseContext.REPLACE }); @@ -1848,7 +1845,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Close replaced active editor unless they match if (!activeReplacement.editor.matches(activeReplacement.replacement)) { if (activeReplacement.forceReplaceDirty) { - this.doCloseEditor(activeReplacement.editor, false, { context: EditorCloseContext.REPLACE }); + this.doCloseEditor(activeReplacement.editor, true, { context: EditorCloseContext.REPLACE }); } else { await this.doCloseEditorWithConfirmationHandling(activeReplacement.editor, { preserveFocus: true }, { context: EditorCloseContext.REPLACE }); } diff --git a/src/vs/workbench/electron-sandbox/actions/windowActions.ts b/src/vs/workbench/electron-sandbox/actions/windowActions.ts index 6535966e5b9..93027940589 100644 --- a/src/vs/workbench/electron-sandbox/actions/windowActions.ts +++ b/src/vs/workbench/electron-sandbox/actions/windowActions.ts @@ -29,7 +29,7 @@ import { isMacintosh } from 'vs/base/common/platform'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { getActiveWindow } from 'vs/base/browser/dom'; -import { isAuxiliaryWindow } from 'vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService'; +import { isAuxiliaryWindow } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; export class CloseWindowAction extends Action2 { diff --git a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts index 8c1a57c8aa8..f29a470f3a8 100644 --- a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts @@ -37,13 +37,21 @@ export interface IAuxiliaryWindow extends IDisposable { readonly onDidLayout: Event; readonly onDidClose: Event; - readonly window: Window & typeof globalThis; + readonly window: AuxiliaryWindow; readonly container: HTMLElement; layout(): void; } -export type AuxiliaryWindow = Window & typeof globalThis; +export type AuxiliaryWindow = Window & typeof globalThis & { + readonly vscodeWindowId: number; +}; + +export function isAuxiliaryWindow(obj: unknown): obj is AuxiliaryWindow { + const candidate = obj as AuxiliaryWindow | undefined; + + return !!candidate && Object.hasOwn(candidate, 'vscodeWindowId'); +} export class BrowserAuxiliaryWindowService extends Disposable implements IAuxiliaryWindowService { @@ -51,6 +59,8 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili private static readonly DEFAULT_SIZE = { width: 800, height: 600 }; + private static WINDOW_IDS = 0; + private readonly _onDidOpenAuxiliaryWindow = this._register(new Emitter()); readonly onDidOpenAuxiliaryWindow = this._onDidOpenAuxiliaryWindow.event; @@ -72,7 +82,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili disposables.add(registerWindow(auxiliaryWindow)); disposables.add(toDisposable(() => auxiliaryWindow.close())); - const { container, onDidLayout, onDidClose } = this.create(auxiliaryWindow, disposables); + const { container, onDidLayout, onDidClose } = await this.create(auxiliaryWindow, disposables); const result: IAuxiliaryWindow = { window: auxiliaryWindow, @@ -118,11 +128,11 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili })).result; } - return auxiliaryWindow?.window; + return auxiliaryWindow?.window as AuxiliaryWindow | undefined; } - protected create(auxiliaryWindow: AuxiliaryWindow, disposables: DisposableStore) { - this.patchMethods(auxiliaryWindow); + protected async create(auxiliaryWindow: AuxiliaryWindow, disposables: DisposableStore) { + await this.patchMethods(auxiliaryWindow); this.applyMeta(auxiliaryWindow); this.applyCSS(auxiliaryWindow, disposables); @@ -263,7 +273,17 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili return { onDidLayout, onDidClose }; } - protected patchMethods(auxiliaryWindow: AuxiliaryWindow): void { + protected async resolveWindowId(auxiliaryWindow: AuxiliaryWindow): Promise { + return BrowserAuxiliaryWindowService.WINDOW_IDS++; + } + + protected async patchMethods(auxiliaryWindow: AuxiliaryWindow): Promise { + + // Add a `vscodeWindowId` property to identify auxiliary windows + const resolvedWindowId = await this.resolveWindowId(auxiliaryWindow); + Object.defineProperty(auxiliaryWindow, 'vscodeWindowId', { + get: () => resolvedWindowId + }); // Disallow `createElement` because it would create // HTML Elements in the "wrong" context and break diff --git a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts index 717e29eabd9..e871ae52974 100644 --- a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts @@ -5,7 +5,7 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { BrowserAuxiliaryWindowService, IAuxiliaryWindowService, AuxiliaryWindow as BaseAuxiliaryWindow } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; +import { BrowserAuxiliaryWindowService, IAuxiliaryWindowService, AuxiliaryWindow as BrowserAuxiliaryWindow } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { ISandboxGlobals } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWindowsConfiguration } from 'vs/platform/window/common/window'; @@ -14,17 +14,10 @@ import { INativeHostService } from 'vs/platform/native/common/native'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { getActiveWindow } from 'vs/base/browser/dom'; -type AuxiliaryWindow = BaseAuxiliaryWindow & { +type NativeAuxiliaryWindow = BrowserAuxiliaryWindow & { readonly vscode: ISandboxGlobals; - readonly vscodeWindowId: number; }; -export function isAuxiliaryWindow(obj: unknown): obj is AuxiliaryWindow { - const candidate = obj as AuxiliaryWindow | undefined; - - return !!candidate?.vscode && Object.hasOwn(candidate, 'vscodeWindowId'); -} - export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService { constructor( @@ -36,7 +29,7 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService super(layoutService, dialogService); } - protected override create(auxiliaryWindow: AuxiliaryWindow, disposables: DisposableStore) { + protected override create(auxiliaryWindow: NativeAuxiliaryWindow, disposables: DisposableStore) { // Zoom level const windowConfig = this.configurationService.getValue(); @@ -46,19 +39,12 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService return super.create(auxiliaryWindow, disposables); } - protected override patchMethods(auxiliaryWindow: AuxiliaryWindow): void { - super.patchMethods(auxiliaryWindow); + protected override resolveWindowId(auxiliaryWindow: NativeAuxiliaryWindow): Promise { + return auxiliaryWindow.vscode.ipcRenderer.invoke('vscode:getWindowId'); + } - // Obtain window identifier - let resolvedWindowId: number; - (async () => { - resolvedWindowId = await auxiliaryWindow.vscode.ipcRenderer.invoke('vscode:getWindowId'); - })(); - - // Add a `windowId` property - Object.defineProperty(auxiliaryWindow, 'vscodeWindowId', { - get: () => resolvedWindowId - }); + protected override async patchMethods(auxiliaryWindow: NativeAuxiliaryWindow): Promise { + await super.patchMethods(auxiliaryWindow); // Enable `window.focus()` to work in Electron by // asking the main process to focus the window. @@ -69,7 +55,7 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService originalWindowFocus(); if (getActiveWindow() !== auxiliaryWindow) { - that.nativeHostService.focusWindow({ targetWindowId: resolvedWindowId }); + that.nativeHostService.focusWindow({ targetWindowId: auxiliaryWindow.vscodeWindowId }); } }; } diff --git a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts index 8909c6bd0f5..430d0c6c649 100644 --- a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts +++ b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts @@ -14,7 +14,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { NativeHostService } from 'vs/platform/native/electron-sandbox/nativeHostService'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService'; -import { isAuxiliaryWindow } from 'vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService'; +import { isAuxiliaryWindow } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { getActiveDocument, getWindowsCount, onDidRegisterWindow, trackFocus } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { memoize } from 'vs/base/common/decorators';