debt - introduce and use dialog main service

This commit is contained in:
Benjamin Pasero
2019-09-30 11:33:40 +02:00
parent f8cdbe7421
commit 65c65e2697
6 changed files with 261 additions and 216 deletions
+9 -4
View File
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { app, ipcMain as ipc, systemPreferences, shell, Event, contentTracing, protocol, powerMonitor, IpcMainEvent } from 'electron';
import { app, ipcMain as ipc, systemPreferences, shell, Event, contentTracing, protocol, powerMonitor, IpcMainEvent, BrowserWindow } from 'electron';
import { IProcessEnvironment, isWindows, isMacintosh } from 'vs/base/common/platform';
import { WindowsManager } from 'vs/code/electron-main/windows';
import { OpenContext, IWindowOpenable } from 'vs/platform/windows/common/windows';
@@ -78,6 +78,8 @@ import { IElectronService } from 'vs/platform/electron/node/electron';
import { ElectronMainService } from 'vs/platform/electron/electron-main/electronMainService';
import { ISharedProcessMainService, SharedProcessMainService } from 'vs/platform/ipc/electron-main/sharedProcessMainService';
import { assign } from 'vs/base/common/objects';
import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
import { withNullAsUndefined } from 'vs/base/common/types';
export class CodeApplication extends Disposable {
@@ -85,6 +87,7 @@ export class CodeApplication extends Disposable {
private static readonly TRUE_MACHINE_ID_KEY = 'telemetry.trueMachineId';
private windowsMainService: IWindowsMainService | undefined;
private dialogMainService: IDialogMainService | undefined;
constructor(
private readonly mainIpcServer: Server,
@@ -449,6 +452,7 @@ export class CodeApplication extends Disposable {
}
services.set(IWindowsMainService, new SyncDescriptor(WindowsManager, [machineId, this.userEnv]));
services.set(IDialogMainService, new SyncDescriptor(DialogMainService));
services.set(ISharedProcessMainService, new SyncDescriptor(SharedProcessMainService, [sharedProcess]));
services.set(ILaunchMainService, new SyncDescriptor(LaunchMainService));
@@ -503,13 +507,13 @@ export class CodeApplication extends Disposable {
contentTracing.stopRecording(join(homedir(), `${product.applicationName}-${Math.random().toString(16).slice(-4)}.trace.txt`), path => {
if (!timeout) {
if (this.windowsMainService) {
this.windowsMainService.showMessageBox({
if (this.dialogMainService) {
this.dialogMainService.showMessageBox({
type: 'info',
message: localize('trace.message', "Successfully created trace."),
detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path),
buttons: [localize('trace.ok', "Ok")]
}, this.windowsMainService.getLastActiveWindow());
}, withNullAsUndefined(BrowserWindow.getFocusedWindow()));
}
} else {
this.logService.info(`Tracing: data recorded (after 30s timeout) to ${path}`);
@@ -580,6 +584,7 @@ export class CodeApplication extends Disposable {
// Propagate to clients
const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService);
this.dialogMainService = accessor.get(IDialogMainService);
// Create a URL handler to open file URIs in the active window
const environmentService = accessor.get(IEnvironmentService);
+23 -198
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import { basename, normalize, join, dirname } from 'vs/base/common/path';
import { basename, normalize, join } from 'vs/base/common/path';
import { localize } from 'vs/nls';
import * as arrays from 'vs/base/common/arrays';
import { assign, mixin } from 'vs/base/common/objects';
@@ -13,7 +13,7 @@ import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
import { IStateService } from 'vs/platform/state/node/state';
import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window';
import { ipcMain as ipc, screen, BrowserWindow, dialog, systemPreferences, FileFilter, shell, MessageBoxReturnValue, MessageBoxOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Display } from 'electron';
import { ipcMain as ipc, screen, BrowserWindow, systemPreferences, MessageBoxOptions, Display } from 'electron';
import { parseLineAndColumnAware } from 'vs/code/node/paths';
import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -27,20 +27,20 @@ import { ITelemetryService, ITelemetryData } from 'vs/platform/telemetry/common/
import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows';
import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService';
import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform';
import { IWorkspaceIdentifier, WORKSPACE_FILTER, isSingleFolderWorkspaceIdentifier, hasWorkspaceFileExtension, IEnterWorkspaceResult, IRecent } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, hasWorkspaceFileExtension, IEnterWorkspaceResult, IRecent } from 'vs/platform/workspaces/common/workspaces';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { Schemas } from 'vs/base/common/network';
import { normalizeNFC } from 'vs/base/common/normalization';
import { URI } from 'vs/base/common/uri';
import { Queue } from 'vs/base/common/async';
import { exists, dirExists } from 'vs/base/node/pfs';
import { dirExists } from 'vs/base/node/pfs';
import { getComparisonKey, isEqual, normalizePath, basename as resourcesBasename, originalFSPath, hasTrailingPathSeparator, removeTrailingPathSeparator } from 'vs/base/common/resources';
import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts';
import { restoreWindowsState, WindowsStateStorageData, getWindowsStateStoreData } from 'vs/code/electron-main/windowsStateStorage';
import { getWorkspaceIdentifier, IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService';
import { once } from 'vs/base/common/functional';
import { Disposable } from 'vs/base/common/lifecycle';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
import { withNullAsUndefined } from 'vs/base/common/types';
const enum WindowError {
UNRESPONSIVE = 1,
@@ -167,7 +167,6 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
private readonly windowsState: IWindowsState;
private lastClosedWindowState?: IWindowState;
private readonly dialogs: Dialogs;
private readonly workspacesManager: WorkspacesManager;
private readonly _onWindowReady = this._register(new Emitter<ICodeWindow>());
@@ -194,7 +193,8 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
@IConfigurationService private readonly configurationService: IConfigurationService,
@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService,
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
@IInstantiationService private readonly instantiationService: IInstantiationService
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IDialogMainService private readonly dialogMainService: IDialogMainService
) {
super();
@@ -203,8 +203,7 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
this.windowsState.openedWindows = [];
}
this.dialogs = new Dialogs(stateService, this);
this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, this);
this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, this, dialogMainService);
this.lifecycleMainService.when(LifecycleMainPhase.Ready).then(() => this.registerListeners());
this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.installWindowsMutex());
@@ -380,12 +379,6 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
};
}
async openExternal(url: string): Promise<boolean> {
shell.openExternal(url);
return true;
}
open(openConfig: IOpenConfiguration): ICodeWindow[] {
this.logService.trace('windowsManager#open');
openConfig = this.validateOpenConfig(openConfig);
@@ -883,7 +876,7 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
noLink: true
};
this.dialogs.showMessageBox(options, this.getFocusedWindow());
this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow()));
}
}
return pathsToOpen;
@@ -1013,10 +1006,12 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
this.logService.error(`Invalid URI input string, scheme missing: ${arg}`);
return undefined;
}
return uri;
} catch (e) {
this.logService.error(`Invalid URI input string: ${arg}, ${e.message}`);
}
return undefined;
}
@@ -1052,6 +1047,7 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
remoteAuthority
};
}
return {
fileUri: uri,
remoteAuthority
@@ -1714,14 +1710,14 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
}
// Show Dialog
this.dialogs.showMessageBox({
this.dialogMainService.showMessageBox({
title: product.nameLong,
type: 'warning',
buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))],
message: localize('appStalled', "The window is no longer responding"),
detail: localize('appStalledDetail', "You can reopen or close the window or keep waiting."),
noLink: true
}, window).then(result => {
}, window.win).then(result => {
if (!window.win) {
return; // Return early if the window has been going down already
}
@@ -1737,14 +1733,14 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
// Crashed
else {
this.dialogs.showMessageBox({
this.dialogMainService.showMessageBox({
title: product.nameLong,
type: 'warning',
buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))],
message: localize('appCrashed', "The window has crashed"),
detail: localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."),
noLink: true
}, window).then(result => {
}, window.win).then(result => {
if (!window.win) {
return; // Return early if the window has been going down already
}
@@ -1774,8 +1770,7 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
}
async pickFileFolderAndOpen(options: INativeOpenDialogOptions, win?: ICodeWindow): Promise<void> {
const title = localize('open', "Open");
const paths = await this.dialogs.pick({ ...options, pickFolders: true, pickFiles: true, title });
const paths = await this.dialogMainService.pickFileFolder(options);
if (paths) {
this.sendPickerTelemetry(paths, options.telemetryEventName || 'openFileFolder', options.telemetryExtraData);
const urisToOpen = await Promise.all(paths.map(async path => {
@@ -1794,8 +1789,7 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
}
async pickFolderAndOpen(options: INativeOpenDialogOptions, win?: ICodeWindow): Promise<void> {
const title = localize('openFolder', "Open Folder");
const paths = await this.dialogs.pick({ ...options, pickFolders: true, title });
const paths = await this.dialogMainService.pickFolder(options);
if (paths) {
this.sendPickerTelemetry(paths, options.telemetryEventName || 'openFolder', options.telemetryExtraData);
this.open({
@@ -1809,8 +1803,7 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
}
async pickFileAndOpen(options: INativeOpenDialogOptions, win?: ICodeWindow): Promise<void> {
const title = localize('openFile', "Open File");
const paths = await this.dialogs.pick({ ...options, pickFiles: true, title });
const paths = await this.dialogMainService.pickFile(options);
if (paths) {
this.sendPickerTelemetry(paths, options.telemetryEventName || 'openFile', options.telemetryExtraData);
this.open({
@@ -1824,10 +1817,7 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
}
async pickWorkspaceAndOpen(options: INativeOpenDialogOptions, win?: ICodeWindow): Promise<void> {
const title = localize('openWorkspaceTitle', "Open Workspace");
const buttonLabel = mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open"));
const filters = WORKSPACE_FILTER;
const paths = await this.dialogs.pick({ ...options, pickFiles: true, title, filters, buttonLabel });
const paths = await this.dialogMainService.pickWorkspace(options);
if (paths) {
this.sendPickerTelemetry(paths, options.telemetryEventName || 'openWorkspace', options.telemetryExtraData);
this.open({
@@ -1842,7 +1832,6 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
}
private sendPickerTelemetry(paths: string[], telemetryEventName: string, telemetryExtraData?: ITelemetryData) {
const numberOfPaths = paths ? paths.length : 0;
// Telemetry
@@ -1854,18 +1843,6 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
});
}
showMessageBox(options: MessageBoxOptions, win?: ICodeWindow): Promise<MessageBoxReturnValue> {
return this.dialogs.showMessageBox(options, win);
}
showSaveDialog(options: SaveDialogOptions, win?: ICodeWindow): Promise<SaveDialogReturnValue> {
return this.dialogs.showSaveDialog(options, win);
}
showOpenDialog(options: OpenDialogOptions, win?: ICodeWindow): Promise<OpenDialogReturnValue> {
return this.dialogs.showOpenDialog(options, win);
}
quit(): void {
// If the user selected to exit from an extension development host window, do not quit, but just
@@ -1884,165 +1861,13 @@ export class WindowsManager extends Disposable implements IWindowsMainService {
}
}
interface IInternalNativeOpenDialogOptions extends INativeOpenDialogOptions {
pickFolders?: boolean;
pickFiles?: boolean;
title: string;
buttonLabel?: string;
filters?: FileFilter[];
}
class Dialogs {
private static readonly workingDirPickerStorageKey = 'pickerWorkingDir';
private readonly mapWindowToDialogQueue: Map<number, Queue<void>>;
private readonly noWindowDialogQueue: Queue<void>;
constructor(
private readonly stateService: IStateService,
private readonly windowsMainService: IWindowsMainService
) {
this.mapWindowToDialogQueue = new Map<number, Queue<void>>();
this.noWindowDialogQueue = new Queue<void>();
}
async pick(options: IInternalNativeOpenDialogOptions, win?: ICodeWindow): Promise<string[] | undefined> {
// Ensure dialog options
const dialogOptions: OpenDialogOptions = {
title: options.title,
buttonLabel: options.buttonLabel,
filters: options.filters
};
// Ensure defaultPath
dialogOptions.defaultPath = options.defaultPath || this.stateService.getItem<string>(Dialogs.workingDirPickerStorageKey);
// Ensure properties
if (typeof options.pickFiles === 'boolean' || typeof options.pickFolders === 'boolean') {
dialogOptions.properties = undefined; // let it override based on the booleans
if (options.pickFiles && options.pickFolders) {
dialogOptions.properties = ['multiSelections', 'openDirectory', 'openFile', 'createDirectory'];
}
}
if (!dialogOptions.properties) {
dialogOptions.properties = ['multiSelections', options.pickFolders ? 'openDirectory' : 'openFile', 'createDirectory'];
}
if (isMacintosh) {
dialogOptions.properties.push('treatPackageAsDirectory'); // always drill into .app files
}
// Show Dialog
const windowToUse = win || this.windowsMainService.getFocusedWindow();
const result = await this.showOpenDialog(dialogOptions, windowToUse);
if (result && result.filePaths && result.filePaths.length > 0) {
// Remember path in storage for next time
this.stateService.setItem(Dialogs.workingDirPickerStorageKey, dirname(result.filePaths[0]));
return result.filePaths;
}
return;
}
private getDialogQueue(window?: ICodeWindow): Queue<any> {
if (!window) {
return this.noWindowDialogQueue;
}
let windowDialogQueue = this.mapWindowToDialogQueue.get(window.id);
if (!windowDialogQueue) {
windowDialogQueue = new Queue<any>();
this.mapWindowToDialogQueue.set(window.id, windowDialogQueue);
}
return windowDialogQueue;
}
showMessageBox(options: MessageBoxOptions, window?: ICodeWindow): Promise<MessageBoxReturnValue> {
return this.getDialogQueue(window).queue(async () => {
if (window) {
return dialog.showMessageBox(window.win, options);
}
return dialog.showMessageBox(options);
});
}
showSaveDialog(options: SaveDialogOptions, window?: ICodeWindow): Promise<SaveDialogReturnValue> {
function normalizePath(path: string | undefined): string | undefined {
if (path && isMacintosh) {
path = normalizeNFC(path); // normalize paths returned from the OS
}
return path;
}
return this.getDialogQueue(window).queue(async () => {
let result: SaveDialogReturnValue;
if (window) {
result = await dialog.showSaveDialog(window.win, options);
} else {
result = await dialog.showSaveDialog(options);
}
result.filePath = normalizePath(result.filePath);
return result;
});
}
showOpenDialog(options: OpenDialogOptions, window?: ICodeWindow): Promise<OpenDialogReturnValue> {
function normalizePaths(paths: string[] | undefined): string[] | undefined {
if (paths && paths.length > 0 && isMacintosh) {
paths = paths.map(path => normalizeNFC(path)); // normalize paths returned from the OS
}
return paths;
}
return this.getDialogQueue(window).queue(async () => {
// Ensure the path exists (if provided)
if (options.defaultPath) {
const pathExists = await exists(options.defaultPath);
if (!pathExists) {
options.defaultPath = undefined;
}
}
// Show dialog
let result: OpenDialogReturnValue;
if (window) {
result = await dialog.showOpenDialog(window.win, options);
} else {
result = await dialog.showOpenDialog(options);
}
result.filePaths = normalizePaths(result.filePaths);
return result;
});
}
}
class WorkspacesManager {
constructor(
private readonly workspacesMainService: IWorkspacesMainService,
private readonly backupMainService: IBackupMainService,
private readonly windowsMainService: IWindowsMainService,
private readonly dialogMainService: IDialogMainService
) { }
async enterWorkspace(window: ICodeWindow, path: URI): Promise<IEnterWorkspaceResult | null> {
@@ -2078,7 +1903,7 @@ class WorkspacesManager {
noLink: true
};
await this.windowsMainService.showMessageBox(options, this.windowsMainService.getFocusedWindow());
await this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow()));
return false;
}