mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
aux window - fix focus issues
This commit is contained in:
committed by
Benjamin Pasero
parent
a6b37b4439
commit
94b453b4f4
@@ -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 <head> 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<HTMLStyleElement>();
|
||||
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<HTMLStyleElement>();
|
||||
globalStylesheets.set(globalStylesheet, clonedGlobalStylesheets);
|
||||
}
|
||||
clonedGlobalStylesheets.add(clone);
|
||||
|
||||
return toDisposable(() => {
|
||||
observer.disconnect();
|
||||
targetWindow.document.head.removeChild(clone);
|
||||
|
||||
globalStylesheets.get(globalStylesheet)?.delete(clone);
|
||||
clonedGlobalStylesheets?.delete(clone);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -37,13 +37,21 @@ export interface IAuxiliaryWindow extends IDisposable {
|
||||
readonly onDidLayout: Event<Dimension>;
|
||||
readonly onDidClose: Event<void>;
|
||||
|
||||
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<IAuxiliaryWindowOpenEvent>());
|
||||
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<number> {
|
||||
return BrowserAuxiliaryWindowService.WINDOW_IDS++;
|
||||
}
|
||||
|
||||
protected async patchMethods(auxiliaryWindow: AuxiliaryWindow): Promise<void> {
|
||||
|
||||
// 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
|
||||
|
||||
+9
-23
@@ -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<IWindowsConfiguration>();
|
||||
@@ -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<number> {
|
||||
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<void> {
|
||||
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 });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user