diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 13ff65cfb4e..1949053395a 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -18,23 +18,38 @@ import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { hash } from 'vs/base/common/hash'; +type WindowGlobal = Window & typeof globalThis; + +interface IWindow { + readonly window: WindowGlobal; + readonly disposables: DisposableStore; +} + export const { registerWindow, getWindows, getWindowsCount, onDidRegisterWindow, onWillUnregisterWindow, onDidUnregisterWindow } = (function () { - const windows = new Set([window]); - const onDidRegisterWindow = new event.Emitter<{ window: Window & typeof globalThis; disposables: DisposableStore }>(); - const onDidUnregisterWindow = new event.Emitter(); - const onWillUnregisterWindow = new event.Emitter(); + const windows = new Map(); + windows.set(window, { window, disposables: new DisposableStore() }); + + const onDidRegisterWindow = new event.Emitter(); + const onDidUnregisterWindow = new event.Emitter(); + const onWillUnregisterWindow = new event.Emitter(); + return { onDidRegisterWindow: onDidRegisterWindow.event, onWillUnregisterWindow: onWillUnregisterWindow.event, onDidUnregisterWindow: onDidUnregisterWindow.event, - registerWindow(window: Window & typeof globalThis): IDisposable { + registerWindow(window: WindowGlobal): IDisposable { if (windows.has(window)) { return Disposable.None; } - windows.add(window); - const disposables = new DisposableStore(); + + const registeredWindow = { + window, + disposables: disposables.add(new DisposableStore()) + }; + windows.set(window, registeredWindow); + disposables.add(toDisposable(() => { windows.delete(window); onDidUnregisterWindow.fire(window); @@ -44,14 +59,12 @@ export const { registerWindow, getWindows, getWindowsCount, onDidRegisterWindow, onWillUnregisterWindow.fire(window); })); - const eventDisposables = new DisposableStore(); - disposables.add(eventDisposables); - onDidRegisterWindow.fire({ window, disposables: eventDisposables }); + onDidRegisterWindow.fire(registeredWindow); return disposables; }, - getWindows(): Iterable { - return windows; + getWindows(): Iterable { + return windows.values(); }, getWindowsCount(): number { return windows.size; @@ -758,19 +771,19 @@ export function getActiveDocument(): Document { return document; } - const documents = Array.from(getWindows()).map(window => window.document); - return documents.find(doc => doc.hasFocus()) ?? document; + const documents = Array.from(getWindows()).map(({ window }) => window.document); + return documents.find(document => document.hasFocus()) ?? document; } -export function getActiveWindow(): Window & typeof globalThis { +export function getActiveWindow(): WindowGlobal { const document = getActiveDocument(); return document.defaultView?.window ?? window; } -export function getWindow(element: Node): Window & typeof globalThis; -export function getWindow(event: UIEvent): Window & typeof globalThis; -export function getWindow(obj: unknown): Window & typeof globalThis; -export function getWindow(e: unknown): Window & typeof globalThis { +export function getWindow(element: Node): WindowGlobal; +export function getWindow(event: UIEvent): WindowGlobal; +export function getWindow(obj: unknown): WindowGlobal; +export function getWindow(e: unknown): WindowGlobal { const candidateNode = e as Node | undefined; if (candidateNode?.ownerDocument?.defaultView) { return candidateNode.ownerDocument.defaultView.window; @@ -805,19 +818,13 @@ 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) { - for (const targetWindow of getWindows()) { + for (const { window: targetWindow, disposables } of getWindows()) { if (targetWindow === window) { continue; // main window is already tracked } - const cloneDisposable = cloneGlobalStyleSheet(style, targetWindow); + const cloneDisposable = disposables.add(cloneGlobalStyleSheet(style, targetWindow)); disposableStore?.add(cloneDisposable); - - disposableStore?.add(event.Event.once(onDidUnregisterWindow)(unregisteredWindow => { - if (unregisteredWindow === targetWindow) { - cloneDisposable.dispose(); - } - })); } } diff --git a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts index 2c45263ca24..28cde30f584 100644 --- a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts +++ b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts @@ -41,6 +41,7 @@ class EditorScopedQuickInputService extends QuickInputService { _serviceBrand: undefined, get hasContainer() { return true; }, get container() { return widget.getDomNode(); }, + getContainer() { return widget.getDomNode(); }, get containers() { return [widget.getDomNode()]; }, get activeContainer() { return widget.getDomNode(); }, get mainContainerDimension() { return editor.getLayoutInfo(); }, diff --git a/src/vs/editor/standalone/browser/standaloneLayoutService.ts b/src/vs/editor/standalone/browser/standaloneLayoutService.ts index 86aeb14e2ab..ae901c68b41 100644 --- a/src/vs/editor/standalone/browser/standaloneLayoutService.ts +++ b/src/vs/editor/standalone/browser/standaloneLayoutService.ts @@ -54,6 +54,10 @@ class StandaloneLayoutService implements ILayoutService { return activeCodeEditor?.getContainerDomNode() ?? this.container; } + getContainer() { + return this.activeContainer; + } + focus(): void { this._codeEditorService.getFocusedCodeEditor()?.focus(); } diff --git a/src/vs/platform/layout/browser/layoutService.ts b/src/vs/platform/layout/browser/layoutService.ts index 8ba84c5d9e5..7ebad2e5fa4 100644 --- a/src/vs/platform/layout/browser/layoutService.ts +++ b/src/vs/platform/layout/browser/layoutService.ts @@ -81,6 +81,11 @@ export interface ILayoutService { */ readonly containers: Iterable; + /** + * Get the container for the given window. + */ + getContainer(window: Window): HTMLElement; + /** * An offset to use for positioning elements inside the main container. */ diff --git a/src/vs/platform/window/electron-sandbox/window.ts b/src/vs/platform/window/electron-sandbox/window.ts index 28968b4f9cc..692dbb9e44c 100644 --- a/src/vs/platform/window/electron-sandbox/window.ts +++ b/src/vs/platform/window/electron-sandbox/window.ts @@ -13,7 +13,7 @@ import { zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; * browser helper so that it can be accessed in non-electron layers. */ export function applyZoom(zoomLevel: number): void { - for (const window of getWindows()) { + for (const { window } of getWindows()) { getGlobals(window)?.webFrame?.setZoomLevel(zoomLevel); } setZoomFactor(zoomLevelToZoomFactor(zoomLevel)); diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index 18d160704a6..14430b7c668 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -9,9 +9,9 @@ import { localize } from 'vs/nls'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { DomEmitter } from 'vs/base/browser/event'; import { Color } from 'vs/base/common/color'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable, toDisposable, dispose, DisposableStore, setDisposableTracker, DisposableTracker, DisposableInfo } from 'vs/base/common/lifecycle'; -import { getDomNodePagePosition, createStyleSheet, createCSSRule, append, $, getActiveDocument } from 'vs/base/browser/dom'; +import { getDomNodePagePosition, createStyleSheet, createCSSRule, append, $, getActiveDocument, onDidRegisterWindow, getWindows } from 'vs/base/browser/dom'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Context } from 'vs/platform/contextkey/browser/contextKeyService'; @@ -130,13 +130,34 @@ class ToggleScreencastModeAction extends Action2 { const disposables = new DisposableStore(); - const container = layoutService.container; + const container = layoutService.activeContainer; + const mouseMarker = append(container, $('.screencast-mouse')); disposables.add(toDisposable(() => mouseMarker.remove())); - const onMouseDown = disposables.add(new DomEmitter(container, 'mousedown', true)); - const onMouseUp = disposables.add(new DomEmitter(container, 'mouseup', true)); - const onMouseMove = disposables.add(new DomEmitter(container, 'mousemove', true)); + const keyboardMarker = append(container, $('.screencast-keyboard')); + disposables.add(toDisposable(() => keyboardMarker.remove())); + + const onMouseDown = disposables.add(new Emitter()); + const onMouseUp = disposables.add(new Emitter()); + const onMouseMove = disposables.add(new Emitter()); + + function registerContainerListeners(container: HTMLElement, disposables: DisposableStore): void { + disposables.add(disposables.add(new DomEmitter(container, 'mousedown', true)).event(e => onMouseDown.fire(e))); + disposables.add(disposables.add(new DomEmitter(container, 'mouseup', true)).event(e => onMouseUp.fire(e))); + disposables.add(disposables.add(new DomEmitter(container, 'mousemove', true)).event(e => onMouseMove.fire(e))); + } + + for (const { window, disposables } of getWindows()) { + registerContainerListeners(layoutService.getContainer(window), disposables); + } + + disposables.add(onDidRegisterWindow(({ window, disposables }) => registerContainerListeners(layoutService.getContainer(window), disposables))); + + disposables.add(layoutService.onDidChangeActiveContainer(() => { + layoutService.activeContainer.appendChild(mouseMarker); + layoutService.activeContainer.appendChild(keyboardMarker); + })); const updateMouseIndicatorColor = () => { mouseMarker.style.borderColor = Color.fromHex(configurationService.getValue('screencastMode.mouseIndicatorColor')).toString(); @@ -172,9 +193,6 @@ class ToggleScreencastModeAction extends Action2 { }); })); - const keyboardMarker = append(container, $('.screencast-keyboard')); - disposables.add(toDisposable(() => keyboardMarker.remove())); - const updateKeyboardFontSize = () => { keyboardMarker.style.fontSize = `${clamp(configurationService.getValue('screencastMode.fontSize') || 56, 20, 100)}px`; }; @@ -214,10 +232,23 @@ class ToggleScreencastModeAction extends Action2 { } })); - const onKeyDown = disposables.add(new DomEmitter(window, 'keydown', true)); - const onCompositionStart = disposables.add(new DomEmitter(window, 'compositionstart', true)); - const onCompositionUpdate = disposables.add(new DomEmitter(window, 'compositionupdate', true)); - const onCompositionEnd = disposables.add(new DomEmitter(window, 'compositionend', true)); + const onKeyDown = disposables.add(new Emitter()); + const onCompositionStart = disposables.add(new Emitter()); + const onCompositionUpdate = disposables.add(new Emitter()); + const onCompositionEnd = disposables.add(new Emitter()); + + function registerWindowListeners(window: Window, disposables: DisposableStore): void { + disposables.add(disposables.add(new DomEmitter(window, 'keydown', true)).event(e => onKeyDown.fire(e))); + disposables.add(disposables.add(new DomEmitter(window, 'compositionstart', true)).event(e => onCompositionStart.fire(e))); + disposables.add(disposables.add(new DomEmitter(window, 'compositionupdate', true)).event(e => onCompositionUpdate.fire(e))); + disposables.add(disposables.add(new DomEmitter(window, 'compositionend', true)).event(e => onCompositionEnd.fire(e))); + } + + for (const { window, disposables } of getWindows()) { + registerWindowListeners(window, disposables); + } + + disposables.add(onDidRegisterWindow(({ window, disposables }) => registerWindowListeners(window, disposables))); let length = 0; let composing: Element | undefined = undefined; diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 19553fffaa9..e0b999dc53d 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -52,7 +52,7 @@ import { IAuxiliaryWindowService, isAuxiliaryWindow } from 'vs/workbench/service //#region Layout Implementation interface ILayoutRuntimeState { - activeContainer: 'main' | number /* window ID */; + activeContainerId: 'main' | number /* window ID */; fullscreen: boolean; maximized: boolean; hasFocus: boolean; @@ -170,7 +170,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi get activeContainer() { return this.getContainerFromDocument(getActiveDocument()); } get containers(): Iterable { const containers: HTMLElement[] = []; - for (const window of getWindows()) { + for (const { window } of getWindows()) { containers.push(this.getContainerFromDocument(window.document)); } @@ -453,8 +453,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private onWindowFocusChanged(hasFocus: boolean): void { if (hasFocus) { const activeContainerId = this.getActiveContainerId(); - if (this.state.runtime.activeContainer !== activeContainerId) { - this.state.runtime.activeContainer = activeContainerId; + if (this.state.runtime.activeContainerId !== activeContainerId) { + this.state.runtime.activeContainerId = activeContainerId; this._onDidChangeActiveContainer.fire(); } } @@ -615,7 +615,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Layout Runtime State const layoutRuntimeState: ILayoutRuntimeState = { - activeContainer: this.getActiveContainerId(), + activeContainerId: this.getActiveContainerId(), fullscreen: isFullscreen(), hasFocus: this.hostService.hasFocus, maximized: false, @@ -1125,12 +1125,19 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } } - getContainer(part: Parts): HTMLElement | undefined { - if (!this.parts.get(part)) { - return undefined; + getContainer(window: Window): HTMLElement; + getContainer(part: Parts): HTMLElement | undefined; + getContainer(windowOrPart: Window | Parts): HTMLElement | undefined { + if (typeof windowOrPart === 'string') { + const part = windowOrPart; + if (!this.parts.get(part)) { + return undefined; + } + + return this.getPart(part).getContainer(); } - return this.getPart(part).getContainer(); + return this.getContainerFromDocument(windowOrPart.document); } isVisible(part: Parts): boolean { diff --git a/src/vs/workbench/electron-sandbox/actions/windowActions.ts b/src/vs/workbench/electron-sandbox/actions/windowActions.ts index 2e1902cd787..71208a301b0 100644 --- a/src/vs/workbench/electron-sandbox/actions/windowActions.ts +++ b/src/vs/workbench/electron-sandbox/actions/windowActions.ts @@ -250,7 +250,7 @@ abstract class BaseSwitchWindow extends Action2 { for (const window of mainWindows) { const auxiliaryWindows = mapMainWindowToAuxiliaryWindows.get(window.id); if (mapMainWindowToAuxiliaryWindows.size > 0) { - picks.push({ type: 'separator', payload: -1, label: auxiliaryWindows ? localize('windowGroup', "Window Group") : undefined } as unknown as IWindowPickItem); + picks.push({ type: 'separator', payload: -1, label: auxiliaryWindows ? localize('windowGroup', "window group") : undefined } as unknown as IWindowPickItem); } const resource = window.filename ? URI.file(window.filename) : isSingleFolderWorkspaceIdentifier(window.workspace) ? window.workspace.uri : isWorkspaceIdentifier(window.workspace) ? window.workspace.configPath : undefined; diff --git a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts index d33a776105a..a3b261ccaa1 100644 --- a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts +++ b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts @@ -59,6 +59,14 @@ class WorkbenchHostService extends Disposable implements IHostService { disposables.add(focusTracker.onDidFocus(() => emitter.fire(this.hasFocus))); disposables.add(focusTracker.onDidBlur(() => emitter.fire(this.hasFocus))); disposables.add(onVisibilityChange.event(() => emitter.fire(this.hasFocus))); + + // Workaround: the window does not immediately seem to have focus when + // opening, so we schedule a check for focus on the next animation frame + window.requestAnimationFrame(() => { + if (window.document.hasFocus()) { + emitter.fire(true); + } + }); })); return emitter.event; diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index 7c44a06d34a..541432be739 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -175,6 +175,7 @@ export interface IWorkbenchLayoutService extends ILayoutService { /** * Returns the parts HTML element, if there is one. */ + getContainer(window: Window): HTMLElement; getContainer(part: Parts): HTMLElement | undefined; /** diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 527aac54ce5..383ebaa90ae 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -623,7 +623,7 @@ export class TestLayoutService implements IWorkbenchLayoutService { getWindowBorderRadius(): string | undefined { return undefined; } isVisible(_part: Parts): boolean { return true; } getDimension(_part: Parts): Dimension { return new Dimension(0, 0); } - getContainer(_part: Parts): HTMLElement { return null!; } + getContainer(): HTMLElement { return null!; } isTitleBarHidden(): boolean { return false; } isStatusBarHidden(): boolean { return false; } isActivityBarHidden(): boolean { return false; }