@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
447 lines (446 loc) • 20 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate =
(this && this.__decorate) ||
function (decorators, target, key, desc) {
var c = arguments.length,
r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc,
d;
if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function')
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param =
(this && this.__param) ||
function (paramIndex, decorator) {
return function (target, key) {
decorator(target, key, paramIndex);
};
};
import { app, screen } from 'electron';
import { Disposable } from '@sussudio/base/common/lifecycle.mjs';
import { isMacintosh } from '@sussudio/base/common/platform.mjs';
import { extUriBiasedIgnorePathCase } from '@sussudio/base/common/resources.mjs';
import { URI } from '@sussudio/base/common/uri.mjs';
import { IConfigurationService } from '../../configuration/common/configuration.mjs';
import { ILifecycleMainService } from '../../lifecycle/electron-main/lifecycleMainService.mjs';
import { ILogService } from '../../log/common/log.mjs';
import { IStateMainService } from '../../state/electron-main/state.mjs';
import { IWindowsMainService } from './windows.mjs';
import { defaultWindowState } from '../../window/electron-main/window.mjs';
import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from '../../workspace/common/workspace.mjs';
let WindowsStateHandler = class WindowsStateHandler extends Disposable {
windowsMainService;
stateMainService;
lifecycleMainService;
logService;
configurationService;
static windowsStateStorageKey = 'windowsState';
get state() {
return this._state;
}
_state = restoreWindowsState(this.stateMainService.getItem(WindowsStateHandler.windowsStateStorageKey));
lastClosedState = undefined;
shuttingDown = false;
constructor(windowsMainService, stateMainService, lifecycleMainService, logService, configurationService) {
super();
this.windowsMainService = windowsMainService;
this.stateMainService = stateMainService;
this.lifecycleMainService = lifecycleMainService;
this.logService = logService;
this.configurationService = configurationService;
this.registerListeners();
}
registerListeners() {
// When a window looses focus, save all windows state. This allows to
// prevent loss of window-state data when OS is restarted without properly
// shutting down the application (https://github.com/microsoft/vscode/issues/87171)
app.on('browser-window-blur', () => {
if (!this.shuttingDown) {
this.saveWindowsState();
}
});
// Handle various lifecycle events around windows
this.lifecycleMainService.onBeforeCloseWindow((window) => this.onBeforeCloseWindow(window));
this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown());
this.windowsMainService.onDidChangeWindowsCount((e) => {
if (e.newCount - e.oldCount > 0) {
// clear last closed window state when a new window opens. this helps on macOS where
// otherwise closing the last window, opening a new window and then quitting would
// use the state of the previously closed window when restarting.
this.lastClosedState = undefined;
}
});
// try to save state before destroy because close will not fire
this.windowsMainService.onDidDestroyWindow((window) => this.onBeforeCloseWindow(window));
}
// Note that onBeforeShutdown() and onBeforeCloseWindow() are fired in different order depending on the OS:
// - macOS: since the app will not quit when closing the last window, you will always first get
// the onBeforeShutdown() event followed by N onBeforeCloseWindow() events for each window
// - other: on other OS, closing the last window will quit the app so the order depends on the
// user interaction: closing the last window will first trigger onBeforeCloseWindow()
// and then onBeforeShutdown(). Using the quit action however will first issue onBeforeShutdown()
// and then onBeforeCloseWindow().
//
// Here is the behavior on different OS depending on action taken (Electron 1.7.x):
//
// Legend
// - quit(N): quit application with N windows opened
// - close(1): close one window via the window close button
// - closeAll: close all windows via the taskbar command
// - onBeforeShutdown(N): number of windows reported in this event handler
// - onBeforeCloseWindow(N, M): number of windows reported and quitRequested boolean in this event handler
//
// macOS
// - quit(1): onBeforeShutdown(1), onBeforeCloseWindow(1, true)
// - quit(2): onBeforeShutdown(2), onBeforeCloseWindow(2, true), onBeforeCloseWindow(2, true)
// - quit(0): onBeforeShutdown(0)
// - close(1): onBeforeCloseWindow(1, false)
//
// Windows
// - quit(1): onBeforeShutdown(1), onBeforeCloseWindow(1, true)
// - quit(2): onBeforeShutdown(2), onBeforeCloseWindow(2, true), onBeforeCloseWindow(2, true)
// - close(1): onBeforeCloseWindow(2, false)[not last window]
// - close(1): onBeforeCloseWindow(1, false), onBeforeShutdown(0)[last window]
// - closeAll(2): onBeforeCloseWindow(2, false), onBeforeCloseWindow(2, false), onBeforeShutdown(0)
//
// Linux
// - quit(1): onBeforeShutdown(1), onBeforeCloseWindow(1, true)
// - quit(2): onBeforeShutdown(2), onBeforeCloseWindow(2, true), onBeforeCloseWindow(2, true)
// - close(1): onBeforeCloseWindow(2, false)[not last window]
// - close(1): onBeforeCloseWindow(1, false), onBeforeShutdown(0)[last window]
// - closeAll(2): onBeforeCloseWindow(2, false), onBeforeCloseWindow(2, false), onBeforeShutdown(0)
//
onBeforeShutdown() {
this.shuttingDown = true;
this.saveWindowsState();
}
saveWindowsState() {
// TODO@electron workaround for Electron not being able to restore
// multiple (native) fullscreen windows on the same display at once
// on macOS.
// https://github.com/electron/electron/issues/34367
const displaysWithFullScreenWindow = new Set();
const currentWindowsState = {
openedWindows: [],
lastPluginDevelopmentHostWindow: this._state.lastPluginDevelopmentHostWindow,
lastActiveWindow: this.lastClosedState,
};
// 1.) Find a last active window (pick any other first window otherwise)
if (!currentWindowsState.lastActiveWindow) {
let activeWindow = this.windowsMainService.getLastActiveWindow();
if (!activeWindow || activeWindow.isExtensionDevelopmentHost) {
activeWindow = this.windowsMainService.getWindows().find((window) => !window.isExtensionDevelopmentHost);
}
if (activeWindow) {
currentWindowsState.lastActiveWindow = this.toWindowState(activeWindow);
if (currentWindowsState.lastActiveWindow.uiState.mode === 3 /* WindowMode.Fullscreen */) {
displaysWithFullScreenWindow.add(currentWindowsState.lastActiveWindow.uiState.display); // always allow fullscreen for active window
}
}
}
// 2.) Find extension host window
const extensionHostWindow = this.windowsMainService
.getWindows()
.find((window) => window.isExtensionDevelopmentHost && !window.isExtensionTestHost);
if (extensionHostWindow) {
currentWindowsState.lastPluginDevelopmentHostWindow = this.toWindowState(extensionHostWindow);
if (currentWindowsState.lastPluginDevelopmentHostWindow.uiState.mode === 3 /* WindowMode.Fullscreen */) {
if (displaysWithFullScreenWindow.has(currentWindowsState.lastPluginDevelopmentHostWindow.uiState.display)) {
if (isMacintosh && !extensionHostWindow.win?.isSimpleFullScreen()) {
currentWindowsState.lastPluginDevelopmentHostWindow.uiState.mode = 1 /* WindowMode.Normal */;
}
} else {
displaysWithFullScreenWindow.add(currentWindowsState.lastPluginDevelopmentHostWindow.uiState.display);
}
}
}
// 3.) All windows (except extension host) for N >= 2 to support `restoreWindows: all` or for auto update
//
// Careful here: asking a window for its window state after it has been closed returns bogus values (width: 0, height: 0)
// so if we ever want to persist the UI state of the last closed window (window count === 1), it has
// to come from the stored lastClosedWindowState on Win/Linux at least
if (this.windowsMainService.getWindowCount() > 1) {
currentWindowsState.openedWindows = this.windowsMainService
.getWindows()
.filter((window) => !window.isExtensionDevelopmentHost)
.map((window) => {
const windowState = this.toWindowState(window);
if (windowState.uiState.mode === 3 /* WindowMode.Fullscreen */) {
if (displaysWithFullScreenWindow.has(windowState.uiState.display)) {
if (
isMacintosh &&
windowState.windowId !== currentWindowsState.lastActiveWindow?.windowId &&
!window.win?.isSimpleFullScreen()
) {
windowState.uiState.mode = 1 /* WindowMode.Normal */;
}
} else {
displaysWithFullScreenWindow.add(windowState.uiState.display);
}
}
return windowState;
});
}
// Persist
const state = getWindowsStateStoreData(currentWindowsState);
this.stateMainService.setItem(WindowsStateHandler.windowsStateStorageKey, state);
if (this.shuttingDown) {
this.logService.trace('[WindowsStateHandler] onBeforeShutdown', state);
}
}
// See note on #onBeforeShutdown() for details how these events are flowing
onBeforeCloseWindow(window) {
if (this.lifecycleMainService.quitRequested) {
return; // during quit, many windows close in parallel so let it be handled in the before-quit handler
}
// On Window close, update our stored UI state of this window
const state = this.toWindowState(window);
if (window.isExtensionDevelopmentHost && !window.isExtensionTestHost) {
this._state.lastPluginDevelopmentHostWindow = state; // do not let test run window state overwrite our extension development state
}
// Any non extension host window with same workspace or folder
else if (!window.isExtensionDevelopmentHost && window.openedWorkspace) {
this._state.openedWindows.forEach((openedWindow) => {
const sameWorkspace =
isWorkspaceIdentifier(window.openedWorkspace) && openedWindow.workspace?.id === window.openedWorkspace.id;
const sameFolder =
isSingleFolderWorkspaceIdentifier(window.openedWorkspace) &&
openedWindow.folderUri &&
extUriBiasedIgnorePathCase.isEqual(openedWindow.folderUri, window.openedWorkspace.uri);
if (sameWorkspace || sameFolder) {
openedWindow.uiState = state.uiState;
}
});
}
// On Windows and Linux closing the last window will trigger quit. Since we are storing all UI state
// before quitting, we need to remember the UI state of this window to be able to persist it.
// On macOS we keep the last closed window state ready in case the user wants to quit right after or
// wants to open another window, in which case we use this state over the persisted one.
if (this.windowsMainService.getWindowCount() === 1) {
this.lastClosedState = state;
}
}
toWindowState(window) {
return {
windowId: window.id,
workspace: isWorkspaceIdentifier(window.openedWorkspace) ? window.openedWorkspace : undefined,
folderUri: isSingleFolderWorkspaceIdentifier(window.openedWorkspace) ? window.openedWorkspace.uri : undefined,
backupPath: window.backupPath,
remoteAuthority: window.remoteAuthority,
uiState: window.serializeWindowState(),
};
}
getNewWindowState(configuration) {
const state = this.doGetNewWindowState(configuration);
const windowConfig = this.configurationService.getValue('window');
// Fullscreen state gets special treatment
if (state.mode === 3 /* WindowMode.Fullscreen */) {
// Window state is not from a previous session: only allow fullscreen if we inherit it or user wants fullscreen
let allowFullscreen;
if (state.hasDefaultState) {
allowFullscreen = !!(
windowConfig?.newWindowDimensions &&
['fullscreen', 'inherit', 'offset'].indexOf(windowConfig.newWindowDimensions) >= 0
);
}
// Window state is from a previous session: only allow fullscreen when we got updated or user wants to restore
else {
allowFullscreen = !!(this.lifecycleMainService.wasRestarted || windowConfig?.restoreFullscreen);
}
if (!allowFullscreen) {
state.mode = 1 /* WindowMode.Normal */;
}
}
return state;
}
doGetNewWindowState(configuration) {
const lastActive = this.windowsMainService.getLastActiveWindow();
// Restore state unless we are running extension tests
if (!configuration.extensionTestsPath) {
// extension development host Window - load from stored settings if any
if (!!configuration.extensionDevelopmentPath && this.state.lastPluginDevelopmentHostWindow) {
return this.state.lastPluginDevelopmentHostWindow.uiState;
}
// Known Workspace - load from stored settings
const workspace = configuration.workspace;
if (isWorkspaceIdentifier(workspace)) {
const stateForWorkspace = this.state.openedWindows
.filter((openedWindow) => openedWindow.workspace && openedWindow.workspace.id === workspace.id)
.map((openedWindow) => openedWindow.uiState);
if (stateForWorkspace.length) {
return stateForWorkspace[0];
}
}
// Known Folder - load from stored settings
if (isSingleFolderWorkspaceIdentifier(workspace)) {
const stateForFolder = this.state.openedWindows
.filter(
(openedWindow) =>
openedWindow.folderUri && extUriBiasedIgnorePathCase.isEqual(openedWindow.folderUri, workspace.uri),
)
.map((openedWindow) => openedWindow.uiState);
if (stateForFolder.length) {
return stateForFolder[0];
}
}
// Empty windows with backups
else if (configuration.backupPath) {
const stateForEmptyWindow = this.state.openedWindows
.filter((openedWindow) => openedWindow.backupPath === configuration.backupPath)
.map((openedWindow) => openedWindow.uiState);
if (stateForEmptyWindow.length) {
return stateForEmptyWindow[0];
}
}
// First Window
const lastActiveState = this.lastClosedState || this.state.lastActiveWindow;
if (!lastActive && lastActiveState) {
return lastActiveState.uiState;
}
}
//
// In any other case, we do not have any stored settings for the window state, so we come up with something smart
//
// We want the new window to open on the same display that the last active one is in
let displayToUse;
const displays = screen.getAllDisplays();
// Single Display
if (displays.length === 1) {
displayToUse = displays[0];
}
// Multi Display
else {
// on mac there is 1 menu per window so we need to use the monitor where the cursor currently is
if (isMacintosh) {
const cursorPoint = screen.getCursorScreenPoint();
displayToUse = screen.getDisplayNearestPoint(cursorPoint);
}
// if we have a last active window, use that display for the new window
if (!displayToUse && lastActive) {
displayToUse = screen.getDisplayMatching(lastActive.getBounds());
}
// fallback to primary display or first display
if (!displayToUse) {
displayToUse = screen.getPrimaryDisplay() || displays[0];
}
}
// Compute x/y based on display bounds
// Note: important to use Math.round() because Electron does not seem to be too happy about
// display coordinates that are not absolute numbers.
let state = defaultWindowState();
state.x = Math.round(displayToUse.bounds.x + displayToUse.bounds.width / 2 - state.width / 2);
state.y = Math.round(displayToUse.bounds.y + displayToUse.bounds.height / 2 - state.height / 2);
// Check for newWindowDimensions setting and adjust accordingly
const windowConfig = this.configurationService.getValue('window');
let ensureNoOverlap = true;
if (windowConfig?.newWindowDimensions) {
if (windowConfig.newWindowDimensions === 'maximized') {
state.mode = 0 /* WindowMode.Maximized */;
ensureNoOverlap = false;
} else if (windowConfig.newWindowDimensions === 'fullscreen') {
state.mode = 3 /* WindowMode.Fullscreen */;
ensureNoOverlap = false;
} else if (
(windowConfig.newWindowDimensions === 'inherit' || windowConfig.newWindowDimensions === 'offset') &&
lastActive
) {
const lastActiveState = lastActive.serializeWindowState();
if (lastActiveState.mode === 3 /* WindowMode.Fullscreen */) {
state.mode = 3 /* WindowMode.Fullscreen */; // only take mode (fixes https://github.com/microsoft/vscode/issues/19331)
} else {
state = lastActiveState;
}
ensureNoOverlap = state.mode !== 3 /* WindowMode.Fullscreen */ && windowConfig.newWindowDimensions === 'offset';
}
}
if (ensureNoOverlap) {
state = this.ensureNoOverlap(state);
}
state.hasDefaultState = true; // flag as default state
return state;
}
ensureNoOverlap(state) {
if (this.windowsMainService.getWindows().length === 0) {
return state;
}
state.x = typeof state.x === 'number' ? state.x : 0;
state.y = typeof state.y === 'number' ? state.y : 0;
const existingWindowBounds = this.windowsMainService.getWindows().map((window) => window.getBounds());
while (existingWindowBounds.some((bounds) => bounds.x === state.x || bounds.y === state.y)) {
state.x += 30;
state.y += 30;
}
return state;
}
};
WindowsStateHandler = __decorate(
[
__param(0, IWindowsMainService),
__param(1, IStateMainService),
__param(2, ILifecycleMainService),
__param(3, ILogService),
__param(4, IConfigurationService),
],
WindowsStateHandler,
);
export { WindowsStateHandler };
export function restoreWindowsState(data) {
const result = { openedWindows: [] };
const windowsState = data || { openedWindows: [] };
if (windowsState.lastActiveWindow) {
result.lastActiveWindow = restoreWindowState(windowsState.lastActiveWindow);
}
if (windowsState.lastPluginDevelopmentHostWindow) {
result.lastPluginDevelopmentHostWindow = restoreWindowState(windowsState.lastPluginDevelopmentHostWindow);
}
if (Array.isArray(windowsState.openedWindows)) {
result.openedWindows = windowsState.openedWindows.map((windowState) => restoreWindowState(windowState));
}
return result;
}
function restoreWindowState(windowState) {
const result = { uiState: windowState.uiState };
if (windowState.backupPath) {
result.backupPath = windowState.backupPath;
}
if (windowState.remoteAuthority) {
result.remoteAuthority = windowState.remoteAuthority;
}
if (windowState.folder) {
result.folderUri = URI.parse(windowState.folder);
}
if (windowState.workspaceIdentifier) {
result.workspace = {
id: windowState.workspaceIdentifier.id,
configPath: URI.parse(windowState.workspaceIdentifier.configURIPath),
};
}
return result;
}
export function getWindowsStateStoreData(windowsState) {
return {
lastActiveWindow: windowsState.lastActiveWindow && serializeWindowState(windowsState.lastActiveWindow),
lastPluginDevelopmentHostWindow:
windowsState.lastPluginDevelopmentHostWindow &&
serializeWindowState(windowsState.lastPluginDevelopmentHostWindow),
openedWindows: windowsState.openedWindows.map((ws) => serializeWindowState(ws)),
};
}
function serializeWindowState(windowState) {
return {
workspaceIdentifier: windowState.workspace && {
id: windowState.workspace.id,
configURIPath: windowState.workspace.configPath.toString(),
},
folder: windowState.folderUri && windowState.folderUri.toString(),
backupPath: windowState.backupPath,
remoteAuthority: windowState.remoteAuthority,
uiState: windowState.uiState,
};
}