mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
aux window - better focus and reveal handling for editors (#194828)
* first cut focus handling
* 💄
* implement moveToTop
* cleanup
This commit is contained in:
@@ -64,6 +64,7 @@ const vscodeResources = [
|
||||
'out-build/vs/base/node/{stdForkStart.js,terminateProcess.sh,cpuUsage.sh,ps.sh}',
|
||||
'out-build/vs/base/browser/ui/codicons/codicon/**',
|
||||
'out-build/vs/base/parts/sandbox/electron-sandbox/preload.js',
|
||||
'out-build/vs/base/parts/sandbox/electron-sandbox/preload-slim.js',
|
||||
'out-build/vs/workbench/browser/media/*-theme.css',
|
||||
'out-build/vs/workbench/contrib/debug/**/*.json',
|
||||
'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt',
|
||||
|
||||
@@ -722,7 +722,7 @@ export function getActiveElement(): Element | null {
|
||||
* Use this instead of `document` when reacting to dom events to handle multiple windows.
|
||||
*/
|
||||
export function getActiveDocument(): Document {
|
||||
const documents = Array.from(getWindows()).map(w => w.document);
|
||||
const documents = Array.from(getWindows()).map(window => window.document);
|
||||
return documents.find(doc => doc.hasFocus()) ?? document;
|
||||
}
|
||||
|
||||
@@ -731,7 +731,10 @@ export function getActiveWindow(): Window & typeof globalThis {
|
||||
return document.defaultView?.window ?? window;
|
||||
}
|
||||
|
||||
function getWindow(e: unknown): Window & typeof globalThis {
|
||||
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 {
|
||||
const candidateNode = e as Node | undefined;
|
||||
if (candidateNode?.ownerDocument?.defaultView) {
|
||||
return candidateNode.ownerDocument.defaultView.window;
|
||||
@@ -745,6 +748,13 @@ function getWindow(e: unknown): Window & typeof globalThis {
|
||||
return window;
|
||||
}
|
||||
|
||||
export function focusWindow(element: Node): void {
|
||||
const window = getWindow(element);
|
||||
if (window !== getActiveWindow()) {
|
||||
window.focus();
|
||||
}
|
||||
}
|
||||
|
||||
export function createStyleSheet(container: HTMLElement = document.getElementsByTagName('head')[0], beforeAppend?: (style: HTMLStyleElement) => void): HTMLStyleElement {
|
||||
const style = document.createElement('style');
|
||||
style.type = 'text/css';
|
||||
|
||||
@@ -7,10 +7,15 @@
|
||||
// #######################################################################
|
||||
// ### ###
|
||||
// ### electron.d.ts types we expose from electron-sandbox ###
|
||||
// ### (copied from Electron 16.x) ###
|
||||
// ### (copied from Electron 25.x) ###
|
||||
// ### ###
|
||||
// #######################################################################
|
||||
|
||||
type Event<Params extends object = {}> = {
|
||||
preventDefault: () => void;
|
||||
readonly defaultPrevented: boolean;
|
||||
} & Params;
|
||||
|
||||
export interface IpcRendererEvent extends Event {
|
||||
|
||||
// Docs: https://electronjs.org/docs/api/structures/ipc-renderer-event
|
||||
@@ -33,12 +38,49 @@ export interface IpcRendererEvent extends Event {
|
||||
* `event.senderId` to `0`.
|
||||
*/
|
||||
senderId: number;
|
||||
/**
|
||||
* Whether the message sent via ipcRenderer.sendTo was sent by the main frame. This
|
||||
* is relevant when `nodeIntegrationInSubFrames` is enabled in the originating
|
||||
* `webContents`.
|
||||
*/
|
||||
senderIsMainFrame?: boolean;
|
||||
}
|
||||
|
||||
export interface IpcRenderer {
|
||||
|
||||
// Docs: https://electronjs.org/docs/api/ipc-renderer
|
||||
|
||||
/**
|
||||
* Resolves with the response from the main process.
|
||||
*
|
||||
* Send a message to the main process via `channel` and expect a result
|
||||
* asynchronously. Arguments will be serialized with the Structured Clone
|
||||
* Algorithm, just like `window.postMessage`, so prototype chains will not be
|
||||
* included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw
|
||||
* an exception.
|
||||
*
|
||||
* The main process should listen for `channel` with `ipcMain.handle()`.
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* If you need to transfer a `MessagePort` to the main process, use
|
||||
* `ipcRenderer.postMessage`.
|
||||
*
|
||||
* If you do not need a response to the message, consider using `ipcRenderer.send`.
|
||||
*
|
||||
* > **Note** Sending non-standard JavaScript types such as DOM objects or special
|
||||
* Electron objects will throw an exception.
|
||||
*
|
||||
* Since the main process does not have support for DOM objects such as
|
||||
* `ImageBitmap`, `File`, `DOMMatrix` and so on, such objects cannot be sent over
|
||||
* Electron's IPC to the main process, as the main process would have no way to
|
||||
* decode them. Attempting to send such objects over IPC will result in an error.
|
||||
*
|
||||
* > **Note** If the handler in the main process throws an error, the promise
|
||||
* returned by `invoke` will reject. However, the `Error` object in the renderer
|
||||
* process will not be the same as the one thrown in the main process.
|
||||
*/
|
||||
invoke(channel: string, ...args: any[]): Promise<any>;
|
||||
/**
|
||||
* Listens to `channel`, when a new message arrives `listener` would be called with
|
||||
* `listener(event, args...)`.
|
||||
@@ -53,6 +95,22 @@ export interface IpcRenderer {
|
||||
* Removes the specified `listener` from the listener array for the specified
|
||||
* `channel`.
|
||||
*/
|
||||
// Note: API with `Transferable` intentionally commented out because you
|
||||
// cannot transfer these when `contextIsolation: true`.
|
||||
// /**
|
||||
// * Send a message to the main process, optionally transferring ownership of zero or
|
||||
// * more `MessagePort` objects.
|
||||
// *
|
||||
// * The transferred `MessagePort` objects will be available in the main process as
|
||||
// * `MessagePortMain` objects by accessing the `ports` property of the emitted
|
||||
// * event.
|
||||
// *
|
||||
// * For example:
|
||||
// *
|
||||
// * For more information on using `MessagePort` and `MessageChannel`, see the MDN
|
||||
// * documentation.
|
||||
// */
|
||||
// postMessage(channel: string, message: any, transfer?: MessagePort[]): void;
|
||||
removeListener(channel: string, listener: (...args: any[]) => void): this;
|
||||
/**
|
||||
* Send an asynchronous message to the main process via `channel`, along with
|
||||
@@ -79,57 +137,14 @@ export interface IpcRenderer {
|
||||
* of a method call, consider using `ipcRenderer.invoke`.
|
||||
*/
|
||||
send(channel: string, ...args: any[]): void;
|
||||
/**
|
||||
* Resolves with the response from the main process.
|
||||
*
|
||||
* Send a message to the main process via `channel` and expect a result
|
||||
* asynchronously. Arguments will be serialized with the Structured Clone
|
||||
* Algorithm, just like `window.postMessage`, so prototype chains will not be
|
||||
* included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw
|
||||
* an exception.
|
||||
*
|
||||
* > **NOTE:** Sending non-standard JavaScript types such as DOM objects or special
|
||||
* Electron objects will throw an exception.
|
||||
*
|
||||
* Since the main process does not have support for DOM objects such as
|
||||
* `ImageBitmap`, `File`, `DOMMatrix` and so on, such objects cannot be sent over
|
||||
* Electron's IPC to the main process, as the main process would have no way to
|
||||
* decode them. Attempting to send such objects over IPC will result in an error.
|
||||
*
|
||||
* The main process should listen for `channel` with `ipcMain.handle()`.
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* If you need to transfer a `MessagePort` to the main process, use
|
||||
* `ipcRenderer.postMessage`.
|
||||
*
|
||||
* If you do not need a response to the message, consider using `ipcRenderer.send`.
|
||||
*/
|
||||
invoke(channel: string, ...args: any[]): Promise<any>;
|
||||
|
||||
// Note: API with `Transferable` intentionally commented out because you
|
||||
// cannot transfer these when `contextIsolation: true`.
|
||||
// /**
|
||||
// * Send a message to the main process, optionally transferring ownership of zero or
|
||||
// * more `MessagePort` objects.
|
||||
// *
|
||||
// * The transferred `MessagePort` objects will be available in the main process as
|
||||
// * `MessagePortMain` objects by accessing the `ports` property of the emitted
|
||||
// * event.
|
||||
// *
|
||||
// * For example:
|
||||
// *
|
||||
// * For more information on using `MessagePort` and `MessageChannel`, see the MDN
|
||||
// * documentation.
|
||||
// */
|
||||
// postMessage(channel: string, message: any, transfer?: MessagePort[]): void;
|
||||
}
|
||||
|
||||
export interface WebFrame {
|
||||
/**
|
||||
* Changes the zoom level to the specified level. The original size is 0 and each
|
||||
* increment above or below represents zooming 20% larger or smaller to default
|
||||
* limits of 300% and 50% of original size, respectively.
|
||||
* limits of 300% and 50% of original size, respectively. The formula for this is
|
||||
* `scale := 1.2 ^ level`.
|
||||
*
|
||||
* > **NOTE**: The zoom policy at the Chromium level is same-origin, meaning that
|
||||
* the zoom level for a specific domain propagates across all instances of windows
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// @ts-check
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const { ipcRenderer, contextBridge } = require('electron');
|
||||
|
||||
/**
|
||||
* @param {string} channel
|
||||
* @returns {true | never}
|
||||
*/
|
||||
function validateIPC(channel) {
|
||||
if (!channel || !channel.startsWith('vscode:')) {
|
||||
throw new Error(`Unsupported event IPC channel '${channel}'`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const globals = {
|
||||
|
||||
/**
|
||||
* A minimal set of methods exposed from Electron's `ipcRenderer`
|
||||
* to support communication to main process.
|
||||
*
|
||||
* @typedef {Pick<import('./electronTypes').IpcRenderer, 'send'>} IpcRenderer
|
||||
*
|
||||
* @type {IpcRenderer}
|
||||
*/
|
||||
|
||||
ipcRenderer: {
|
||||
|
||||
/**
|
||||
* @param {string} channel
|
||||
* @param {any[]} args
|
||||
*/
|
||||
send(channel, ...args) {
|
||||
if (validateIPC(channel)) {
|
||||
ipcRenderer.send(channel, ...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('vscode', globals);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}());
|
||||
@@ -84,8 +84,8 @@ import { NativeURLService } from 'vs/platform/url/common/urlService';
|
||||
import { ElectronURLListener } from 'vs/platform/url/electron-main/electronUrlListener';
|
||||
import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService';
|
||||
import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMainService';
|
||||
import { IWindowOpenable, IWindowSettings, zoomLevelToZoomFactor } from 'vs/platform/window/common/window';
|
||||
import { defaultBrowserWindowOptions, IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows';
|
||||
import { IWindowOpenable } from 'vs/platform/window/common/window';
|
||||
import { IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows';
|
||||
import { ICodeWindow } from 'vs/platform/window/electron-main/window';
|
||||
import { WindowsMainService } from 'vs/platform/windows/electron-main/windowsMainService';
|
||||
import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker';
|
||||
@@ -118,6 +118,7 @@ import { ElectronPtyHostStarter } from 'vs/platform/terminal/electron-main/elect
|
||||
import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService';
|
||||
import { NODE_REMOTE_RESOURCE_CHANNEL_NAME, NODE_REMOTE_RESOURCE_IPC_METHOD_NAME, NodeRemoteResourceResponse, NodeRemoteResourceRouter } from 'vs/platform/remote/common/electronRemoteResources';
|
||||
import { Lazy } from 'vs/base/common/lazy';
|
||||
import { AuxiliaryWindow } from 'vs/platform/windows/electron-main/auxiliaryWindow';
|
||||
|
||||
/**
|
||||
* The main VS Code application. There will only ever be one instance,
|
||||
@@ -382,6 +383,12 @@ export class CodeApplication extends Disposable {
|
||||
//
|
||||
app.on('web-contents-created', (event, contents) => {
|
||||
|
||||
// Child Window: delegate to `AuxiliaryWindow` class
|
||||
const isChildWindow = contents?.opener?.url.startsWith(`${Schemas.vscodeFileResource}://${VSCODE_AUTHORITY}/`);
|
||||
if (isChildWindow) {
|
||||
this.mainInstantiationService.createInstance(AuxiliaryWindow, contents);
|
||||
}
|
||||
|
||||
// Block any in-page navigation
|
||||
contents.on('will-navigate', event => {
|
||||
this.logService.error('webContents#will-navigate: Prevented webcontent navigation');
|
||||
@@ -389,21 +396,6 @@ export class CodeApplication extends Disposable {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// Child Window: apply zoom after loading finished and handle --open-devtools
|
||||
const isChildWindow = contents?.opener?.url.startsWith(`${Schemas.vscodeFileResource}://${VSCODE_AUTHORITY}/`);
|
||||
if (isChildWindow) {
|
||||
contents.on('dom-ready', () => {
|
||||
const windowZoomLevel = this.configurationService.getValue<IWindowSettings | undefined>('window')?.zoomLevel ?? 0;
|
||||
|
||||
contents.setZoomLevel(windowZoomLevel);
|
||||
contents.setZoomFactor(zoomLevelToZoomFactor(windowZoomLevel));
|
||||
});
|
||||
|
||||
if (this.environmentMainService.args['open-devtools'] === true) {
|
||||
contents.openDevTools({ mode: 'bottom' });
|
||||
}
|
||||
}
|
||||
|
||||
// All Windows: only allow about:blank child windows to open
|
||||
// For all other URLs, delegate to the OS.
|
||||
contents.setWindowOpenHandler(handler => {
|
||||
@@ -414,7 +406,7 @@ export class CodeApplication extends Disposable {
|
||||
|
||||
return {
|
||||
action: 'allow',
|
||||
overrideBrowserWindowOptions: this.mainInstantiationService.invokeFunction(defaultBrowserWindowOptions)
|
||||
overrideBrowserWindowOptions: AuxiliaryWindow.open(this.mainInstantiationService)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ export interface ICommonNativeHostService {
|
||||
maximizeWindow(): Promise<void>;
|
||||
unmaximizeWindow(): Promise<void>;
|
||||
minimizeWindow(): Promise<void>;
|
||||
moveWindowTop(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Only supported on Windows and macOS. Updates the window controls to match the title bar size.
|
||||
|
||||
@@ -213,6 +213,13 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
|
||||
}
|
||||
}
|
||||
|
||||
async moveWindowTop(windowId: number | undefined): Promise<void> {
|
||||
const window = this.windowById(windowId);
|
||||
if (window?.win) {
|
||||
window.win.moveTop();
|
||||
}
|
||||
}
|
||||
|
||||
async updateWindowControls(windowId: number | undefined, options: { height?: number; backgroundColor?: string; foregroundColor?: string }): Promise<void> {
|
||||
const window = this.windowById(windowId);
|
||||
if (window) {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions, WebContents } from 'electron';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IWindowSettings, zoomLevelToZoomFactor } from 'vs/platform/window/common/window';
|
||||
import { defaultBrowserWindowOptions } from 'vs/platform/windows/electron-main/windows';
|
||||
|
||||
export class AuxiliaryWindow {
|
||||
|
||||
static open(instantiationService: IInstantiationService): BrowserWindowConstructorOptions {
|
||||
return instantiationService.invokeFunction(defaultBrowserWindowOptions, undefined, {
|
||||
webPreferences: {
|
||||
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-sandbox/preload-slim.js').fsPath
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly contents: WebContents,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService
|
||||
) {
|
||||
|
||||
this.create();
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private create(): void {
|
||||
|
||||
// Apply zoom level when DOM is ready
|
||||
this.contents.on('dom-ready', () => {
|
||||
const windowZoomLevel = this.configurationService.getValue<IWindowSettings | undefined>('window')?.zoomLevel ?? 0;
|
||||
|
||||
this.contents.setZoomLevel(windowZoomLevel);
|
||||
this.contents.setZoomFactor(zoomLevelToZoomFactor(windowZoomLevel));
|
||||
});
|
||||
|
||||
// Handle devtools argument
|
||||
if (this.environmentMainService.args['open-devtools'] === true) {
|
||||
this.contents.openDevTools({ mode: 'bottom' });
|
||||
}
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
||||
// Support a small set of IPC calls
|
||||
this.contents.ipc.on('vscode:focusAuxiliaryWindow', () => {
|
||||
this.withWindow(window => window.focus(), true /* restore */);
|
||||
});
|
||||
|
||||
this.contents.ipc.on('vscode:moveAuxiliaryWindowTop', () => {
|
||||
this.withWindow(window => window.moveTop(), true /* restore */);
|
||||
});
|
||||
}
|
||||
|
||||
private withWindow(callback: (window: BrowserWindow) => void, restore?: boolean): void {
|
||||
const window = BrowserWindow.fromWebContents(this.contents);
|
||||
if (window) {
|
||||
if (restore) {
|
||||
if (window.isMinimized()) {
|
||||
window.restore();
|
||||
}
|
||||
}
|
||||
|
||||
callback(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { IComposite, ICompositeControl } from 'vs/workbench/common/composite';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IConstructorSignature, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { trackFocus, Dimension, IDomPosition } from 'vs/base/browser/dom';
|
||||
import { trackFocus, Dimension, IDomPosition, focusWindow } from 'vs/base/browser/dom';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { assertIsDefined } from 'vs/base/common/types';
|
||||
@@ -148,7 +148,13 @@ export abstract class Composite extends Component implements IComposite {
|
||||
* Called when this composite should receive keyboard focus.
|
||||
*/
|
||||
focus(): void {
|
||||
// Subclasses can implement
|
||||
const container = this.getContainer();
|
||||
if (container) {
|
||||
// Make sure to focus the window of the container
|
||||
// because it is possible that the composite is
|
||||
// opened in a auxiliary window that is not focussed.
|
||||
focusWindow(container);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -149,6 +149,8 @@ export abstract class PaneComposite extends Composite implements IPaneComposite
|
||||
|
||||
override focus(): void {
|
||||
this.viewPaneContainer?.focus();
|
||||
|
||||
super.focus();
|
||||
}
|
||||
|
||||
protected abstract createViewPaneContainer(parent: HTMLElement): ViewPaneContainer;
|
||||
|
||||
@@ -10,7 +10,7 @@ import Severity from 'vs/base/common/severity';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { EditorExtensions, EditorInputCapabilities, IEditorOpenContext, IVisibleEditorPane, isEditorOpenError } from 'vs/workbench/common/editor';
|
||||
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
|
||||
import { Dimension, show, hide, IDomNodePagePosition, isAncestor } from 'vs/base/browser/dom';
|
||||
import { Dimension, show, hide, IDomNodePagePosition, isAncestor, getWindow, getActiveWindow } from 'vs/base/browser/dom';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IEditorPaneRegistry, IEditorPaneDescriptor } from 'vs/workbench/browser/editor';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
@@ -27,6 +27,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IDialogService, IPromptButton, IPromptCancelButton } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash';
|
||||
import { IHostService } from 'vs/workbench/services/host/browser/host';
|
||||
|
||||
export interface IOpenEditorResult {
|
||||
|
||||
@@ -99,7 +100,8 @@ export class EditorPanes extends Disposable {
|
||||
@IEditorProgressService private readonly editorProgressService: IEditorProgressService,
|
||||
@IWorkspaceTrustManagementService private readonly workspaceTrustService: IWorkspaceTrustManagementService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IDialogService private readonly dialogService: IDialogService
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
@IHostService private readonly hostService: IHostService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -251,10 +253,18 @@ export class EditorPanes extends Disposable {
|
||||
// Apply input to pane
|
||||
const { changed, cancelled } = await this.doSetInput(pane, editor, options, context);
|
||||
|
||||
// Focus only if not cancelled and not prevented
|
||||
const focus = !options || !options.preserveFocus;
|
||||
if (!cancelled && focus && this.shouldRestoreFocus(activeElement)) {
|
||||
pane.focus();
|
||||
// Make sure to pass focus to the pane or otherwise
|
||||
// make sure that the pane window is visible.
|
||||
if (!cancelled) {
|
||||
const focus = !options || !options.preserveFocus;
|
||||
if (focus && this.shouldRestoreFocus(activeElement)) {
|
||||
pane.focus();
|
||||
} else {
|
||||
const paneWindow = getWindow(pane.getContainer());
|
||||
if (paneWindow !== getActiveWindow()) {
|
||||
this.hostService.moveTop(paneWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { pane, changed, cancelled };
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Dimension, size, clearNode, $, EventHelper } from 'vs/base/browser/dom'
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { assertIsDefined, assertAllDefined } from 'vs/base/common/types';
|
||||
import { assertAllDefined } from 'vs/base/common/types';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IWorkspaceContextService, isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
|
||||
import { EditorOpenSource, IEditorOptions } from 'vs/platform/editor/common/editor';
|
||||
@@ -166,9 +166,9 @@ export abstract class EditorPlaceholder extends EditorPane {
|
||||
}
|
||||
|
||||
override focus(): void {
|
||||
const container = assertIsDefined(this.container);
|
||||
this.container?.focus();
|
||||
|
||||
container.focus();
|
||||
super.focus();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
|
||||
@@ -419,6 +419,8 @@ export class SideBySideEditor extends AbstractEditorWithViewState<ISideBySideEdi
|
||||
|
||||
override focus(): void {
|
||||
this.getLastFocusedEditorPane()?.focus();
|
||||
|
||||
super.focus();
|
||||
}
|
||||
|
||||
private getLastFocusedEditorPane(): EditorPane | undefined {
|
||||
|
||||
@@ -90,6 +90,8 @@ export abstract class AbstractTextCodeEditor<T extends IEditorViewState> extends
|
||||
|
||||
override focus(): void {
|
||||
this.editorControl?.focus();
|
||||
|
||||
super.focus();
|
||||
}
|
||||
|
||||
override hasFocus(): boolean {
|
||||
|
||||
@@ -339,6 +339,8 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp
|
||||
|
||||
override focus(): void {
|
||||
this.diffEditorControl?.focus();
|
||||
|
||||
super.focus();
|
||||
}
|
||||
|
||||
override hasFocus(): boolean {
|
||||
|
||||
@@ -75,6 +75,8 @@ export class ChatEditor extends EditorPane {
|
||||
if (this.widget) {
|
||||
this.widget.focusInput();
|
||||
}
|
||||
|
||||
super.focus();
|
||||
}
|
||||
|
||||
override clearInput(): void {
|
||||
|
||||
@@ -624,6 +624,8 @@ export class ExtensionEditor extends EditorPane {
|
||||
|
||||
override focus(): void {
|
||||
this.activeElement?.focus();
|
||||
|
||||
super.focus();
|
||||
}
|
||||
|
||||
showFind(): void {
|
||||
|
||||
@@ -675,6 +675,8 @@ export class InteractiveEditor extends EditorPane {
|
||||
override focus() {
|
||||
this._notebookWidget.value?.onShow();
|
||||
this._codeEditorWidget.focus();
|
||||
|
||||
super.focus();
|
||||
}
|
||||
|
||||
focusHistory() {
|
||||
|
||||
@@ -454,6 +454,8 @@ export class MergeEditor extends AbstractTextEditor<IMergeEditorViewState> {
|
||||
|
||||
override focus(): void {
|
||||
(this.getControl() ?? this.inputResultView.editor).focus();
|
||||
|
||||
super.focus();
|
||||
}
|
||||
|
||||
override hasFocus(): boolean {
|
||||
|
||||
@@ -967,10 +967,6 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD
|
||||
super.setEditorVisible(visible, group);
|
||||
}
|
||||
|
||||
override focus() {
|
||||
super.focus();
|
||||
}
|
||||
|
||||
override clearInput(): void {
|
||||
super.clearInput();
|
||||
|
||||
|
||||
@@ -192,6 +192,8 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP
|
||||
} else if (!isIOS) {
|
||||
this.searchWidget.focus();
|
||||
}
|
||||
|
||||
super.focus();
|
||||
}
|
||||
|
||||
get activeKeybindingEntry(): IKeybindingItemEntry | null {
|
||||
|
||||
@@ -493,6 +493,8 @@ export class SettingsEditor2 extends EditorPane {
|
||||
} else if (this._currentFocusContext === SettingsFocusContext.TableOfContents) {
|
||||
this.tocTree.domFocus();
|
||||
}
|
||||
|
||||
super.focus();
|
||||
}
|
||||
|
||||
protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void {
|
||||
|
||||
@@ -278,6 +278,8 @@ export class SearchEditor extends AbstractTextCodeEditor<SearchEditorViewState>
|
||||
} else {
|
||||
this.queryEditorWidget.focus();
|
||||
}
|
||||
|
||||
super.focus();
|
||||
}
|
||||
|
||||
focusSearchInput() {
|
||||
|
||||
@@ -98,6 +98,8 @@ export class TerminalEditor extends EditorPane {
|
||||
|
||||
override focus() {
|
||||
this._editorInput?.terminalInstance?.focus();
|
||||
|
||||
super.focus();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
|
||||
@@ -1586,6 +1586,8 @@ export class GettingStartedPage extends EditorPane {
|
||||
// This prevents us from stealing back focus from other focused elements such as quick pick due to delayed load.
|
||||
this.container.focus();
|
||||
}
|
||||
|
||||
super.focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -232,6 +232,8 @@ export class WalkThroughPart extends EditorPane {
|
||||
(this.lastFocus || this.content).focus();
|
||||
}
|
||||
this.editorFocus.set(true);
|
||||
|
||||
super.focus();
|
||||
}
|
||||
|
||||
arrowUp() {
|
||||
|
||||
@@ -747,6 +747,8 @@ export class WorkspaceTrustEditor extends EditorPane {
|
||||
|
||||
override focus() {
|
||||
this.rootElement.focus();
|
||||
|
||||
super.focus();
|
||||
}
|
||||
|
||||
override async setInput(input: WorkspaceTrustEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
|
||||
|
||||
@@ -32,7 +32,9 @@ export interface IAuxiliaryWindow extends IDisposable {
|
||||
readonly container: HTMLElement;
|
||||
}
|
||||
|
||||
export class AuxiliaryWindowService implements IAuxiliaryWindowService {
|
||||
type AuxiliaryWindow = Window & typeof globalThis;
|
||||
|
||||
export class BrowserAuxiliaryWindowService implements IAuxiliaryWindowService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
@@ -45,11 +47,11 @@ export class AuxiliaryWindowService implements IAuxiliaryWindowService {
|
||||
open(): IAuxiliaryWindow {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
const auxiliaryWindow = assertIsDefined(window.open('about:blank')?.window);
|
||||
const auxiliaryWindow = assertIsDefined(window.open('about:blank')?.window) as AuxiliaryWindow;
|
||||
disposables.add(registerWindow(auxiliaryWindow));
|
||||
disposables.add(toDisposable(() => auxiliaryWindow.close()));
|
||||
|
||||
this.blockMethods(auxiliaryWindow);
|
||||
this.patchMethods(auxiliaryWindow);
|
||||
|
||||
this.applyMeta(auxiliaryWindow);
|
||||
this.applyCSS(auxiliaryWindow, disposables);
|
||||
@@ -69,7 +71,7 @@ export class AuxiliaryWindowService implements IAuxiliaryWindowService {
|
||||
};
|
||||
}
|
||||
|
||||
private applyMeta(auxiliaryWindow: Window): void {
|
||||
private applyMeta(auxiliaryWindow: AuxiliaryWindow): void {
|
||||
const metaCharset = auxiliaryWindow.document.head.appendChild(document.createElement('meta'));
|
||||
metaCharset.setAttribute('charset', 'utf-8');
|
||||
|
||||
@@ -80,7 +82,7 @@ export class AuxiliaryWindowService implements IAuxiliaryWindowService {
|
||||
}
|
||||
}
|
||||
|
||||
private applyCSS(auxiliaryWindow: Window, disposables: DisposableStore): void {
|
||||
private applyCSS(auxiliaryWindow: AuxiliaryWindow, disposables: DisposableStore): void {
|
||||
|
||||
// Clone all style elements and stylesheet links from the window to the child window
|
||||
for (const element of document.head.querySelectorAll('link[rel="stylesheet"], style')) {
|
||||
@@ -107,7 +109,7 @@ export class AuxiliaryWindowService implements IAuxiliaryWindowService {
|
||||
}
|
||||
}
|
||||
|
||||
private applyHTML(auxiliaryWindow: Window, disposables: DisposableStore): HTMLElement {
|
||||
private applyHTML(auxiliaryWindow: AuxiliaryWindow, disposables: DisposableStore): HTMLElement {
|
||||
|
||||
// Create workbench container and apply classes
|
||||
const container = document.createElement('div');
|
||||
@@ -121,7 +123,7 @@ export class AuxiliaryWindowService implements IAuxiliaryWindowService {
|
||||
return container;
|
||||
}
|
||||
|
||||
private registerListeners(auxiliaryWindow: Window & typeof globalThis, container: HTMLElement, disposables: DisposableStore) {
|
||||
private registerListeners(auxiliaryWindow: AuxiliaryWindow, container: HTMLElement, disposables: DisposableStore) {
|
||||
const onDidClose = disposables.add(new Emitter<void>());
|
||||
disposables.add(addDisposableListener(auxiliaryWindow, 'unload', () => {
|
||||
onDidClose.fire();
|
||||
@@ -153,11 +155,15 @@ export class AuxiliaryWindowService implements IAuxiliaryWindowService {
|
||||
return { onDidResize, onDidClose };
|
||||
}
|
||||
|
||||
private blockMethods(auxiliaryWindow: Window): void {
|
||||
protected patchMethods(auxiliaryWindow: AuxiliaryWindow): void {
|
||||
|
||||
// Disallow `createElement` because it would create
|
||||
// HTML Elements in the "wrong" context and break
|
||||
// code that does "instanceof HTMLElement" etc.
|
||||
auxiliaryWindow.document.createElement = function () {
|
||||
throw new Error('Not allowed to create elements in child window JavaScript context. Always use the main window so that "xyz instanceof HTMLElement" continues to work.');
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IAuxiliaryWindowService, AuxiliaryWindowService, InstantiationType.Delayed);
|
||||
registerSingleton(IAuxiliaryWindowService, BrowserAuxiliaryWindowService, InstantiationType.Delayed);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
import { BrowserAuxiliaryWindowService, IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService';
|
||||
|
||||
type AuxiliaryWindow = Window & typeof globalThis & {
|
||||
vscode: {
|
||||
ipcRenderer: Pick<import('vs/base/parts/sandbox/electron-sandbox/electronTypes').IpcRenderer, 'send'>;
|
||||
};
|
||||
moveTop: () => void;
|
||||
};
|
||||
|
||||
export function isAuxiliaryWindow(obj: unknown): obj is AuxiliaryWindow {
|
||||
const candidate = obj as AuxiliaryWindow | undefined;
|
||||
|
||||
return typeof candidate?.vscode?.ipcRenderer?.send === 'function';
|
||||
}
|
||||
|
||||
export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService {
|
||||
|
||||
constructor(
|
||||
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@ILifecycleService lifecycleService: ILifecycleService
|
||||
) {
|
||||
super(layoutService, environmentService, lifecycleService);
|
||||
}
|
||||
|
||||
protected override patchMethods(auxiliaryWindow: AuxiliaryWindow): void {
|
||||
super.patchMethods(auxiliaryWindow);
|
||||
|
||||
// Enable `window.focus()` to work in Electron by
|
||||
// asking the main process to focus the window.
|
||||
const originalWindowFocus = auxiliaryWindow.focus.bind(auxiliaryWindow);
|
||||
auxiliaryWindow.focus = function () {
|
||||
originalWindowFocus();
|
||||
|
||||
auxiliaryWindow.vscode.ipcRenderer.send('vscode:focusAuxiliaryWindow');
|
||||
};
|
||||
|
||||
// Add a method to move window to the top (TODO@bpasero better to go entirely through native host service)
|
||||
Object.defineProperty(auxiliaryWindow, 'moveTop', {
|
||||
value: () => {
|
||||
auxiliaryWindow.vscode.ipcRenderer.send('vscode:moveAuxiliaryWindowTop');
|
||||
},
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IAuxiliaryWindowService, NativeAuxiliaryWindowService, InstantiationType.Delayed);
|
||||
@@ -501,6 +501,10 @@ export class BrowserHostService extends Disposable implements IHostService {
|
||||
}
|
||||
}
|
||||
|
||||
async moveTop(window: Window & typeof globalThis): Promise<void> {
|
||||
// There seems to be no API to bring a window to front in browsers
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
@@ -69,6 +69,11 @@ export interface IHostService {
|
||||
*/
|
||||
toggleFullScreen(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Bring a window to the front and restore it if needed.
|
||||
*/
|
||||
moveTop(window: Window & typeof globalThis): Promise<void>;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
@@ -14,6 +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';
|
||||
|
||||
class WorkbenchNativeHostService extends NativeHostService {
|
||||
|
||||
@@ -114,6 +115,16 @@ class WorkbenchHostService extends Disposable implements IHostService {
|
||||
return this.nativeHostService.toggleFullScreen();
|
||||
}
|
||||
|
||||
async moveTop(win: Window & typeof globalThis): Promise<void> {
|
||||
if (win === window) {
|
||||
return this.nativeHostService.moveWindowTop();
|
||||
}
|
||||
|
||||
if (isAuxiliaryWindow(win)) {
|
||||
return win.moveTop();
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
|
||||
@@ -1463,6 +1463,7 @@ export class TestHostService implements IHostService {
|
||||
}
|
||||
|
||||
async focus(options?: { force: boolean }): Promise<void> { }
|
||||
async moveTop(): Promise<void> { }
|
||||
|
||||
async openWindow(arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise<void> { }
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ export class TestNativeHostService implements INativeHostService {
|
||||
async maximizeWindow(): Promise<void> { }
|
||||
async unmaximizeWindow(): Promise<void> { }
|
||||
async minimizeWindow(): Promise<void> { }
|
||||
async moveWindowTop(): Promise<void> { }
|
||||
async updateWindowControls(options: { height?: number; backgroundColor?: string; foregroundColor?: string }): Promise<void> { }
|
||||
async setMinimumSize(width: number | undefined, height: number | undefined): Promise<void> { }
|
||||
async saveWindowSplash(value: IPartsSplash): Promise<void> { }
|
||||
|
||||
@@ -114,7 +114,6 @@ import 'vs/workbench/services/textMate/browser/textMateTokenizationFeature.contr
|
||||
import 'vs/workbench/services/userActivity/common/userActivityService';
|
||||
import 'vs/workbench/services/userActivity/browser/userActivityBrowser';
|
||||
import 'vs/workbench/services/issue/browser/issueTroubleshoot';
|
||||
import 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService';
|
||||
|
||||
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
|
||||
|
||||
@@ -88,6 +88,7 @@ import 'vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistorySer
|
||||
import 'vs/workbench/services/userDataSync/browser/userDataSyncEnablementService';
|
||||
import 'vs/workbench/services/extensions/electron-sandbox/nativeExtensionService';
|
||||
import 'vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService';
|
||||
import 'vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService';
|
||||
|
||||
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit';
|
||||
|
||||
@@ -63,6 +63,7 @@ import 'vs/workbench/services/userDataSync/browser/webUserDataSyncEnablementServ
|
||||
import 'vs/workbench/services/userDataProfile/browser/userDataProfileStorageService';
|
||||
import 'vs/workbench/services/configurationResolver/browser/configurationResolverService';
|
||||
import 'vs/platform/extensionResourceLoader/browser/extensionResourceLoaderService';
|
||||
import 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService';
|
||||
|
||||
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
|
||||
|
||||
Reference in New Issue
Block a user