From 65c65e26976ebb16ae4e406ea0aed85fd1a71b4c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 30 Sep 2019 11:33:40 +0200 Subject: [PATCH] debt - introduce and use dialog main service --- src/vs/code/electron-main/app.ts | 13 +- src/vs/code/electron-main/windows.ts | 221 ++---------------- .../platform/dialogs/electron-main/dialogs.ts | 207 ++++++++++++++++ .../electron-main/electronMainService.ts | 21 +- .../issue/electron-main/issueMainService.ts | 9 +- .../platform/windows/electron-main/windows.ts | 6 +- 6 files changed, 261 insertions(+), 216 deletions(-) create mode 100644 src/vs/platform/dialogs/electron-main/dialogs.ts diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index e538687891f..fae9a6441c6 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -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); diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 284e1185178..f48a831a40f 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -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()); @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - return this.dialogs.showMessageBox(options, win); - } - - showSaveDialog(options: SaveDialogOptions, win?: ICodeWindow): Promise { - return this.dialogs.showSaveDialog(options, win); - } - - showOpenDialog(options: OpenDialogOptions, win?: ICodeWindow): Promise { - 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>; - private readonly noWindowDialogQueue: Queue; - - constructor( - private readonly stateService: IStateService, - private readonly windowsMainService: IWindowsMainService - ) { - this.mapWindowToDialogQueue = new Map>(); - this.noWindowDialogQueue = new Queue(); - } - - async pick(options: IInternalNativeOpenDialogOptions, win?: ICodeWindow): Promise { - - // Ensure dialog options - const dialogOptions: OpenDialogOptions = { - title: options.title, - buttonLabel: options.buttonLabel, - filters: options.filters - }; - - // Ensure defaultPath - dialogOptions.defaultPath = options.defaultPath || this.stateService.getItem(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 { - if (!window) { - return this.noWindowDialogQueue; - } - - let windowDialogQueue = this.mapWindowToDialogQueue.get(window.id); - if (!windowDialogQueue) { - windowDialogQueue = new Queue(); - this.mapWindowToDialogQueue.set(window.id, windowDialogQueue); - } - - return windowDialogQueue; - } - - showMessageBox(options: MessageBoxOptions, window?: ICodeWindow): Promise { - return this.getDialogQueue(window).queue(async () => { - if (window) { - return dialog.showMessageBox(window.win, options); - } - - return dialog.showMessageBox(options); - }); - } - - showSaveDialog(options: SaveDialogOptions, window?: ICodeWindow): Promise { - - 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 { - - 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 { @@ -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; } diff --git a/src/vs/platform/dialogs/electron-main/dialogs.ts b/src/vs/platform/dialogs/electron-main/dialogs.ts new file mode 100644 index 00000000000..84430fe2820 --- /dev/null +++ b/src/vs/platform/dialogs/electron-main/dialogs.ts @@ -0,0 +1,207 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { MessageBoxOptions, MessageBoxReturnValue, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, dialog, FileFilter, BrowserWindow } from 'electron'; +import { Queue } from 'vs/base/common/async'; +import { IStateService } from 'vs/platform/state/node/state'; +import { isMacintosh } from 'vs/base/common/platform'; +import { dirname } from 'path'; +import { normalizeNFC } from 'vs/base/common/normalization'; +import { exists } from 'vs/base/node/pfs'; +import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { localize } from 'vs/nls'; +import { WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces'; +import { mnemonicButtonLabel } from 'vs/base/common/labels'; + +export const IDialogMainService = createDecorator('dialogMainService'); + +export interface IDialogMainService { + + _serviceBrand: undefined; + + pickFileFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise; + pickFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise; + pickFile(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise; + pickWorkspace(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise; + + showMessageBox(options: MessageBoxOptions, window?: BrowserWindow): Promise; + showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise; + showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise; +} + +interface IInternalNativeOpenDialogOptions extends INativeOpenDialogOptions { + pickFolders?: boolean; + pickFiles?: boolean; + + title: string; + buttonLabel?: string; + filters?: FileFilter[]; +} + +export class DialogMainService implements IDialogMainService { + + _serviceBrand: undefined; + + private static readonly workingDirPickerStorageKey = 'pickerWorkingDir'; + + private readonly mapWindowToDialogQueue: Map>; + private readonly noWindowDialogQueue: Queue; + + constructor( + @IStateService private readonly stateService: IStateService + ) { + this.mapWindowToDialogQueue = new Map>(); + this.noWindowDialogQueue = new Queue(); + } + + pickFileFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise { + return this.doPick({ ...options, pickFolders: true, pickFiles: true, title: localize('open', "Open") }, window); + } + + pickFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise { + return this.doPick({ ...options, pickFolders: true, title: localize('openFolder', "Open Folder") }, window); + } + + pickFile(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise { + return this.doPick({ ...options, pickFiles: true, title: localize('openFile', "Open File") }, window); + } + + pickWorkspace(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise { + const title = localize('openWorkspaceTitle', "Open Workspace"); + const buttonLabel = mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open")); + const filters = WORKSPACE_FILTER; + + return this.doPick({ ...options, pickFiles: true, title, filters, buttonLabel }, window); + } + + private async doPick(options: IInternalNativeOpenDialogOptions, window?: BrowserWindow): Promise { + + // Ensure dialog options + const dialogOptions: OpenDialogOptions = { + title: options.title, + buttonLabel: options.buttonLabel, + filters: options.filters + }; + + // Ensure defaultPath + dialogOptions.defaultPath = options.defaultPath || this.stateService.getItem(DialogMainService.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 = window || BrowserWindow.getFocusedWindow(); + + const result = await this.showOpenDialog(dialogOptions, withNullAsUndefined(windowToUse)); + if (result && result.filePaths && result.filePaths.length > 0) { + + // Remember path in storage for next time + this.stateService.setItem(DialogMainService.workingDirPickerStorageKey, dirname(result.filePaths[0])); + + return result.filePaths; + } + + return; + } + + private getDialogQueue(window?: BrowserWindow): Queue { + if (!window) { + return this.noWindowDialogQueue; + } + + let windowDialogQueue = this.mapWindowToDialogQueue.get(window.id); + if (!windowDialogQueue) { + windowDialogQueue = new Queue(); + this.mapWindowToDialogQueue.set(window.id, windowDialogQueue); + } + + return windowDialogQueue; + } + + showMessageBox(options: MessageBoxOptions, window?: BrowserWindow): Promise { + return this.getDialogQueue(window).queue(async () => { + if (window) { + return dialog.showMessageBox(window, options); + } + + return dialog.showMessageBox(options); + }); + } + + showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise { + + 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, options); + } else { + result = await dialog.showSaveDialog(options); + } + + result.filePath = normalizePath(result.filePath); + + return result; + }); + } + + showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise { + + 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, options); + } else { + result = await dialog.showOpenDialog(options); + } + + result.filePaths = normalizePaths(result.filePaths); + + return result; + }); + } +} diff --git a/src/vs/platform/electron/electron-main/electronMainService.ts b/src/vs/platform/electron/electron-main/electronMainService.ts index d78b9d4dba6..6069c84a5ca 100644 --- a/src/vs/platform/electron/electron-main/electronMainService.ts +++ b/src/vs/platform/electron/electron-main/electronMainService.ts @@ -15,6 +15,7 @@ import { IElectronService } from 'vs/platform/electron/node/electron'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { AddFirstParameterToFunctions } from 'vs/base/common/types'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; export class ElectronMainService implements AddFirstParameterToFunctions /* only methods, not events */, number /* window ID */> { @@ -22,6 +23,7 @@ export class ElectronMainService implements AddFirstParameterToFunctions { - return this.windowsMainService.showMessageBox(options, this.windowsMainService.getWindowById(windowId)); + return this.dialogMainService.showMessageBox(options, this.toBrowserWindow(windowId)); } async showSaveDialog(windowId: number, options: SaveDialogOptions): Promise { - return this.windowsMainService.showSaveDialog(options, this.windowsMainService.getWindowById(windowId)); + return this.dialogMainService.showSaveDialog(options, this.toBrowserWindow(windowId)); } async showOpenDialog(windowId: number, options: OpenDialogOptions): Promise { - return this.windowsMainService.showOpenDialog(options, this.windowsMainService.getWindowById(windowId)); + return this.dialogMainService.showOpenDialog(options, this.toBrowserWindow(windowId)); + } + + private toBrowserWindow(windowId: number): BrowserWindow | undefined { + const window = this.windowsMainService.getWindowById(windowId); + if (window) { + return window.win; + } + + return undefined; } async pickFileFolderAndOpen(windowId: number, options: INativeOpenDialogOptions): Promise { @@ -224,7 +235,9 @@ export class ElectronMainService implements AddFirstParameterToFunctions { - return this.windowsMainService.openExternal(url); + shell.openExternal(url); + + return true; } async updateTouchBar(windowId: number, items: ISerializableCommandAction[][]): Promise { diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index 44a2a82e0bb..6cbc7a83f6b 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -7,14 +7,14 @@ import { localize } from 'vs/nls'; import * as objects from 'vs/base/common/objects'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { IIssueService, IssueReporterData, IssueReporterFeatures, ProcessExplorerData } from 'vs/platform/issue/node/issue'; -import { BrowserWindow, ipcMain, screen, dialog, IpcMainEvent, Display } from 'electron'; +import { BrowserWindow, ipcMain, screen, dialog, IpcMainEvent, Display, shell } from 'electron'; import { ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService'; import { PerformanceInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; -import { IWindowState, IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; +import { IWindowState } from 'vs/platform/windows/electron-main/windows'; import { listProcesses } from 'vs/base/node/ps'; const DEFAULT_BACKGROUND_COLOR = '#1E1E1E'; @@ -32,8 +32,7 @@ export class IssueMainService implements IIssueService { @IEnvironmentService private readonly environmentService: IEnvironmentService, @ILaunchMainService private readonly launchMainService: ILaunchMainService, @ILogService private readonly logService: ILogService, - @IDiagnosticsService private readonly diagnosticsService: IDiagnosticsService, - @IWindowsMainService private readonly windowsMainService: IWindowsMainService + @IDiagnosticsService private readonly diagnosticsService: IDiagnosticsService ) { this.registerListeners(); } @@ -146,7 +145,7 @@ export class IssueMainService implements IIssueService { }); ipcMain.on('vscode:openExternal', (_: unknown, arg: string) => { - this.windowsMainService.openExternal(arg); + shell.openExternal(arg); }); ipcMain.on('vscode:closeIssueReporter', (event: IpcMainEvent) => { diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index a38c507db37..02b9440c32e 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -12,7 +12,7 @@ import { IProcessEnvironment } from 'vs/base/common/platform'; import { IWorkspaceIdentifier, IEnterWorkspaceResult } from 'vs/platform/workspaces/common/workspaces'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { URI } from 'vs/base/common/uri'; -import { MessageBoxReturnValue, SaveDialogReturnValue, OpenDialogReturnValue, Rectangle, BrowserWindow, MessageBoxOptions, SaveDialogOptions, OpenDialogOptions } from 'electron'; +import { Rectangle, BrowserWindow } from 'electron'; export interface IWindowState { width?: number; @@ -104,15 +104,11 @@ export interface IWindowsMainService { pickFolderAndOpen(options: INativeOpenDialogOptions, win?: ICodeWindow): Promise; pickFileAndOpen(options: INativeOpenDialogOptions, win?: ICodeWindow): Promise; pickWorkspaceAndOpen(options: INativeOpenDialogOptions, win?: ICodeWindow): Promise; - showMessageBox(options: MessageBoxOptions, win?: ICodeWindow): Promise; - showSaveDialog(options: SaveDialogOptions, win?: ICodeWindow): Promise; - showOpenDialog(options: OpenDialogOptions, win?: ICodeWindow): Promise; focusLastActive(cli: ParsedArgs, context: OpenContext): ICodeWindow; getLastActiveWindow(): ICodeWindow | undefined; waitForWindowCloseOrLoad(windowId: number): Promise; openEmptyWindow(context: OpenContext, options?: IOpenEmptyWindowOptions): ICodeWindow[]; openNewTabbedWindow(context: OpenContext): ICodeWindow[]; - openExternal(url: string): Promise; sendToFocused(channel: string, ...args: any[]): void; sendToAll(channel: string, payload: any, windowIdsToIgnore?: number[]): void; getFocusedWindow(): ICodeWindow | undefined;