Aux window: Enable screencast mode support (fix #196087) (#197047)

This commit is contained in:
Benjamin Pasero
2023-10-31 09:10:23 +01:00
committed by GitHub
parent 5240f7230a
commit 8e991e784c
11 changed files with 116 additions and 52 deletions
+34 -27
View File
@@ -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<Window & typeof globalThis>();
const onWillUnregisterWindow = new event.Emitter<Window & typeof globalThis>();
const windows = new Map<WindowGlobal, IWindow>();
windows.set(window, { window, disposables: new DisposableStore() });
const onDidRegisterWindow = new event.Emitter<IWindow>();
const onDidUnregisterWindow = new event.Emitter<WindowGlobal>();
const onWillUnregisterWindow = new event.Emitter<WindowGlobal>();
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<Window & typeof globalThis> {
return windows;
getWindows(): Iterable<IWindow> {
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 <head> 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();
}
}));
}
}
@@ -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(); },
@@ -54,6 +54,10 @@ class StandaloneLayoutService implements ILayoutService {
return activeCodeEditor?.getContainerDomNode() ?? this.container;
}
getContainer() {
return this.activeContainer;
}
focus(): void {
this._codeEditorService.getFocusedCodeEditor()?.focus();
}
@@ -81,6 +81,11 @@ export interface ILayoutService {
*/
readonly containers: Iterable<HTMLElement>;
/**
* Get the container for the given window.
*/
getContainer(window: Window): HTMLElement;
/**
* An offset to use for positioning elements inside the main container.
*/
@@ -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));
@@ -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<MouseEvent>());
const onMouseUp = disposables.add(new Emitter<MouseEvent>());
const onMouseMove = disposables.add(new Emitter<MouseEvent>());
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<string>('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<number>('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<KeyboardEvent>());
const onCompositionStart = disposables.add(new Emitter<CompositionEvent>());
const onCompositionUpdate = disposables.add(new Emitter<CompositionEvent>());
const onCompositionEnd = disposables.add(new Emitter<CompositionEvent>());
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;
+16 -9
View File
@@ -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<HTMLElement> {
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 {
@@ -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;
@@ -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;
@@ -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;
/**
@@ -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; }