Path: blob/main/src/vs/workbench/services/host/electron-browser/nativeHostService.ts
5240 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter, Event } from '../../../../base/common/event.js';6import { IHostService, IToastOptions, IToastResult } from '../browser/host.js';7import { FocusMode, INativeHostService } from '../../../../platform/native/common/native.js';8import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';9import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js';10import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';11import { IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, IOpenEmptyWindowOptions, IPoint, IRectangle, IOpenedAuxiliaryWindow, IOpenedMainWindow } from '../../../../platform/window/common/window.js';12import { Disposable, DisposableSet, IDisposable } from '../../../../base/common/lifecycle.js';13import { NativeHostService } from '../../../../platform/native/common/nativeHostService.js';14import { INativeWorkbenchEnvironmentService } from '../../environment/electron-browser/environmentService.js';15import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';16import { disposableWindowInterval, getActiveDocument, getWindowId, getWindowsCount, hasWindow, onDidRegisterWindow } from '../../../../base/browser/dom.js';17import { memoize } from '../../../../base/common/decorators.js';18import { isAuxiliaryWindow } from '../../../../base/browser/window.js';19import { VSBuffer } from '../../../../base/common/buffer.js';20import { CancellationToken } from '../../../../base/common/cancellation.js';21import { showBrowserToast } from '../browser/toasts.js';22import { generateUuid } from '../../../../base/common/uuid.js';2324class WorkbenchNativeHostService extends NativeHostService {2526constructor(27@INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService,28@IMainProcessService mainProcessService: IMainProcessService29) {30super(environmentService.window.id, mainProcessService);31}32}3334class WorkbenchHostService extends Disposable implements IHostService {3536declare readonly _serviceBrand: undefined;3738constructor(39@INativeHostService private readonly nativeHostService: INativeHostService,40@ILabelService private readonly labelService: ILabelService,41@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService42) {43super();4445this.onDidChangeFocus = Event.latch(46Event.any(47Event.map(Event.filter(this.nativeHostService.onDidFocusMainOrAuxiliaryWindow, id => hasWindow(id), this._store), () => this.hasFocus, this._store),48Event.map(Event.filter(this.nativeHostService.onDidBlurMainOrAuxiliaryWindow, id => hasWindow(id), this._store), () => this.hasFocus, this._store),49Event.map(this.onDidChangeActiveWindow, () => this.hasFocus, this._store)50), undefined, this._store51);5253this.onDidChangeFullScreen = Event.filter(this.nativeHostService.onDidChangeWindowFullScreen, e => hasWindow(e.windowId), this._store);5455this.registerListeners();56}5758private registerListeners(): void {5960// Make sure to hide all OS toasts when the window gains focus61this._register(this.onDidChangeFocus(focus => {62if (focus) {63this.clearToasts();64}65}));66}6768//#region Focus6970readonly onDidChangeFocus: Event<boolean>;7172get hasFocus(): boolean {73return getActiveDocument().hasFocus();74}7576async hadLastFocus(): Promise<boolean> {77const activeWindowId = await this.nativeHostService.getActiveWindowId();7879if (typeof activeWindowId === 'undefined') {80return false;81}8283return activeWindowId === this.nativeHostService.windowId;84}8586//#endregion8788//#region Window8990@memoize91get onDidChangeActiveWindow(): Event<number> {92const emitter = this._register(new Emitter<number>());9394// Emit via native focus tracking95this._register(Event.filter(this.nativeHostService.onDidFocusMainOrAuxiliaryWindow, id => hasWindow(id), this._store)(id => emitter.fire(id)));9697this._register(onDidRegisterWindow(({ window, disposables }) => {9899// Emit via interval: immediately when opening an auxiliary window,100// it is possible that document focus has not yet changed, so we101// poll for a while to ensure we catch the event.102disposables.add(disposableWindowInterval(window, () => {103const hasFocus = window.document.hasFocus();104if (hasFocus) {105emitter.fire(window.vscodeWindowId);106}107108return hasFocus;109}, 100, 20));110}));111112return Event.latch(emitter.event, undefined, this._store);113}114115readonly onDidChangeFullScreen: Event<{ readonly windowId: number; readonly fullscreen: boolean }>;116117openWindow(options?: IOpenEmptyWindowOptions): Promise<void>;118openWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise<void>;119openWindow(arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise<void> {120if (Array.isArray(arg1)) {121return this.doOpenWindow(arg1, arg2);122}123124return this.doOpenEmptyWindow(arg1);125}126127private doOpenWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise<void> {128const remoteAuthority = this.environmentService.remoteAuthority;129if (remoteAuthority) {130toOpen.forEach(openable => openable.label = openable.label || this.getRecentLabel(openable));131132if (options?.remoteAuthority === undefined) {133// set the remoteAuthority of the window the request came from.134// It will be used when the input is neither file nor vscode-remote.135options = options ? { ...options, remoteAuthority } : { remoteAuthority };136}137}138139return this.nativeHostService.openWindow(toOpen, options);140}141142private getRecentLabel(openable: IWindowOpenable): string {143if (isFolderToOpen(openable)) {144return this.labelService.getWorkspaceLabel(openable.folderUri, { verbose: Verbosity.LONG });145}146147if (isWorkspaceToOpen(openable)) {148return this.labelService.getWorkspaceLabel({ id: '', configPath: openable.workspaceUri }, { verbose: Verbosity.LONG });149}150151return this.labelService.getUriLabel(openable.fileUri, { appendWorkspaceSuffix: true });152}153154private doOpenEmptyWindow(options?: IOpenEmptyWindowOptions): Promise<void> {155const remoteAuthority = this.environmentService.remoteAuthority;156if (!!remoteAuthority && options?.remoteAuthority === undefined) {157// set the remoteAuthority of the window the request came from158options = options ? { ...options, remoteAuthority } : { remoteAuthority };159}160return this.nativeHostService.openWindow(options);161}162163toggleFullScreen(targetWindow: Window): Promise<void> {164return this.nativeHostService.toggleFullScreen({ targetWindowId: isAuxiliaryWindow(targetWindow) ? targetWindow.vscodeWindowId : undefined });165}166167async moveTop(targetWindow: Window): Promise<void> {168if (getWindowsCount() <= 1) {169return; // does not apply when only one window is opened170}171172return this.nativeHostService.moveWindowTop(isAuxiliaryWindow(targetWindow) ? { targetWindowId: targetWindow.vscodeWindowId } : undefined);173}174175getCursorScreenPoint(): Promise<{ readonly point: IPoint; readonly display: IRectangle }> {176return this.nativeHostService.getCursorScreenPoint();177}178179getWindows(options: { includeAuxiliaryWindows: true }): Promise<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>>;180getWindows(options: { includeAuxiliaryWindows: false }): Promise<Array<IOpenedMainWindow>>;181getWindows(options: { includeAuxiliaryWindows: boolean }): Promise<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>> {182if (options.includeAuxiliaryWindows === false) {183return this.nativeHostService.getWindows({ includeAuxiliaryWindows: false });184}185186return this.nativeHostService.getWindows({ includeAuxiliaryWindows: true });187}188189//#endregion190191//#region Lifecycle192193focus(targetWindow: Window, options?: { mode?: FocusMode }): Promise<void> {194return this.nativeHostService.focusWindow({195mode: options?.mode,196targetWindowId: getWindowId(targetWindow)197});198}199200restart(): Promise<void> {201return this.nativeHostService.relaunch();202}203204reload(options?: { disableExtensions?: boolean }): Promise<void> {205return this.nativeHostService.reload(options);206}207208close(): Promise<void> {209return this.nativeHostService.closeWindow();210}211212async withExpectedShutdown<T>(expectedShutdownTask: () => Promise<T>): Promise<T> {213return await expectedShutdownTask();214}215216//#endregion217218//#region Screenshots219220getScreenshot(rect?: IRectangle): Promise<VSBuffer | undefined> {221return this.nativeHostService.getScreenshot(rect);222}223224//#endregion225226//#region Native Handle227228private _nativeWindowHandleCache = new Map<number, Promise<VSBuffer | undefined>>();229async getNativeWindowHandle(windowId: number): Promise<VSBuffer | undefined> {230if (!this._nativeWindowHandleCache.has(windowId)) {231this._nativeWindowHandleCache.set(windowId, this.nativeHostService.getNativeWindowHandle(windowId));232}233return this._nativeWindowHandleCache.get(windowId)!;234}235236//#endregion237238//#region Toast Notifications239240private readonly activeBrowserToasts = this._register(new DisposableSet());241242async showToast(options: IToastOptions, token: CancellationToken): Promise<IToastResult> {243const id = generateUuid();244const listener = token.onCancellationRequested(() => this.nativeHostService.clearToast(id));245246try {247// Try native OS notifications first248const nativeToast = await this.nativeHostService.showToast({ ...options, id });249if (nativeToast.supported) {250return nativeToast;251}252253// Then fallback to browser notifications254return await showBrowserToast({255onDidCreateToast: (toast: IDisposable) => this.activeBrowserToasts.add(toast),256onDidDisposeToast: (toast: IDisposable) => this.activeBrowserToasts.deleteAndDispose(toast)257}, options, token);258} finally {259listener.dispose();260}261}262263private async clearToasts(): Promise<void> {264await this.nativeHostService.clearToasts();265266this.activeBrowserToasts.clearAndDisposeAll();267}268269//#endregion270}271272registerSingleton(IHostService, WorkbenchHostService, InstantiationType.Delayed);273registerSingleton(INativeHostService, WorkbenchNativeHostService, InstantiationType.Delayed);274275276