Path: blob/main/src/vs/platform/dialogs/electron-main/dialogMainService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import electron from 'electron';6import { Queue } from '../../../base/common/async.js';7import { hash } from '../../../base/common/hash.js';8import { mnemonicButtonLabel } from '../../../base/common/labels.js';9import { Disposable, dispose, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';10import { normalizeNFC } from '../../../base/common/normalization.js';11import { isMacintosh } from '../../../base/common/platform.js';12import { Promises } from '../../../base/node/pfs.js';13import { localize } from '../../../nls.js';14import { INativeOpenDialogOptions, massageMessageBoxOptions } from '../common/dialogs.js';15import { createDecorator } from '../../instantiation/common/instantiation.js';16import { ILogService } from '../../log/common/log.js';17import { IProductService } from '../../product/common/productService.js';18import { WORKSPACE_FILTER } from '../../workspace/common/workspace.js';1920export const IDialogMainService = createDecorator<IDialogMainService>('dialogMainService');2122export interface IDialogMainService {2324readonly _serviceBrand: undefined;2526pickFileFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined>;27pickFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined>;28pickFile(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined>;29pickWorkspace(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined>;3031showMessageBox(options: electron.MessageBoxOptions, window?: electron.BrowserWindow): Promise<electron.MessageBoxReturnValue>;32showSaveDialog(options: electron.SaveDialogOptions, window?: electron.BrowserWindow): Promise<electron.SaveDialogReturnValue>;33showOpenDialog(options: electron.OpenDialogOptions, window?: electron.BrowserWindow): Promise<electron.OpenDialogReturnValue>;34}3536interface IInternalNativeOpenDialogOptions extends INativeOpenDialogOptions {37readonly pickFolders?: boolean;38readonly pickFiles?: boolean;3940readonly title: string;41readonly buttonLabel?: string;42readonly filters?: electron.FileFilter[];43}4445export class DialogMainService implements IDialogMainService {4647declare readonly _serviceBrand: undefined;4849private readonly windowFileDialogLocks = new Map<number, Set<number>>();50private readonly windowDialogQueues = new Map<number, Queue<electron.MessageBoxReturnValue | electron.SaveDialogReturnValue | electron.OpenDialogReturnValue>>();51private readonly noWindowDialogueQueue = new Queue<electron.MessageBoxReturnValue | electron.SaveDialogReturnValue | electron.OpenDialogReturnValue>();5253constructor(54@ILogService private readonly logService: ILogService,55@IProductService private readonly productService: IProductService56) {57}5859pickFileFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined> {60return this.doPick({ ...options, pickFolders: true, pickFiles: true, title: localize('open', "Open") }, window);61}6263pickFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined> {64return this.doPick({ ...options, pickFolders: true, title: localize('openFolder', "Open Folder") }, window);65}6667pickFile(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined> {68return this.doPick({ ...options, pickFiles: true, title: localize('openFile', "Open File") }, window);69}7071pickWorkspace(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined> {72const title = localize('openWorkspaceTitle', "Open Workspace from File");73const buttonLabel = mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open")).withMnemonic;74const filters = WORKSPACE_FILTER;7576return this.doPick({ ...options, pickFiles: true, title, filters, buttonLabel }, window);77}7879private async doPick(options: IInternalNativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined> {8081// Ensure dialog options82const dialogOptions: electron.OpenDialogOptions = {83title: options.title,84buttonLabel: options.buttonLabel,85filters: options.filters,86defaultPath: options.defaultPath87};8889// Ensure properties90if (typeof options.pickFiles === 'boolean' || typeof options.pickFolders === 'boolean') {91dialogOptions.properties = undefined; // let it override based on the booleans9293if (options.pickFiles && options.pickFolders) {94dialogOptions.properties = ['multiSelections', 'openDirectory', 'openFile', 'createDirectory'];95}96}9798if (!dialogOptions.properties) {99dialogOptions.properties = ['multiSelections', options.pickFolders ? 'openDirectory' : 'openFile', 'createDirectory'];100}101102if (isMacintosh) {103dialogOptions.properties.push('treatPackageAsDirectory'); // always drill into .app files104}105106// Show Dialog107const result = await this.showOpenDialog(dialogOptions, (window || electron.BrowserWindow.getFocusedWindow()) ?? undefined);108if (result && result.filePaths && result.filePaths.length > 0) {109return result.filePaths;110}111112return undefined;113}114115private getWindowDialogQueue<T extends electron.MessageBoxReturnValue | electron.SaveDialogReturnValue | electron.OpenDialogReturnValue>(window?: electron.BrowserWindow): Queue<T> {116117// Queue message box requests per window so that one can show118// after the other.119if (window) {120let windowDialogQueue = this.windowDialogQueues.get(window.id);121if (!windowDialogQueue) {122windowDialogQueue = new Queue<electron.MessageBoxReturnValue | electron.SaveDialogReturnValue | electron.OpenDialogReturnValue>();123this.windowDialogQueues.set(window.id, windowDialogQueue);124}125126return windowDialogQueue as unknown as Queue<T>;127} else {128return this.noWindowDialogueQueue as unknown as Queue<T>;129}130}131132showMessageBox(rawOptions: electron.MessageBoxOptions, window?: electron.BrowserWindow): Promise<electron.MessageBoxReturnValue> {133return this.getWindowDialogQueue<electron.MessageBoxReturnValue>(window).queue(async () => {134const { options, buttonIndeces } = massageMessageBoxOptions(rawOptions, this.productService);135136let result: electron.MessageBoxReturnValue | undefined = undefined;137if (window) {138result = await electron.dialog.showMessageBox(window, options);139} else {140result = await electron.dialog.showMessageBox(options);141}142143return {144response: buttonIndeces[result.response],145checkboxChecked: result.checkboxChecked146};147});148}149150async showSaveDialog(options: electron.SaveDialogOptions, window?: electron.BrowserWindow): Promise<electron.SaveDialogReturnValue> {151152// Prevent duplicates of the same dialog queueing at the same time153const fileDialogLock = this.acquireFileDialogLock(options, window);154if (!fileDialogLock) {155this.logService.error('[DialogMainService]: file save dialog is already or will be showing for the window with the same configuration');156157return { canceled: true, filePath: '' };158}159160try {161return await this.getWindowDialogQueue<electron.SaveDialogReturnValue>(window).queue(async () => {162let result: electron.SaveDialogReturnValue;163if (window) {164result = await electron.dialog.showSaveDialog(window, options);165} else {166result = await electron.dialog.showSaveDialog(options);167}168169result.filePath = this.normalizePath(result.filePath);170171return result;172});173} finally {174dispose(fileDialogLock);175}176}177178private normalizePath(path: string): string;179private normalizePath(path: string | undefined): string | undefined;180private normalizePath(path: string | undefined): string | undefined {181if (path && isMacintosh) {182path = normalizeNFC(path); // macOS only: normalize paths to NFC form183}184185return path;186}187188private normalizePaths(paths: string[]): string[] {189return paths.map(path => this.normalizePath(path));190}191192async showOpenDialog(options: electron.OpenDialogOptions, window?: electron.BrowserWindow): Promise<electron.OpenDialogReturnValue> {193194// Ensure the path exists (if provided)195if (options.defaultPath) {196const pathExists = await Promises.exists(options.defaultPath);197if (!pathExists) {198options.defaultPath = undefined;199}200}201202// Prevent duplicates of the same dialog queueing at the same time203const fileDialogLock = this.acquireFileDialogLock(options, window);204if (!fileDialogLock) {205this.logService.error('[DialogMainService]: file open dialog is already or will be showing for the window with the same configuration');206207return { canceled: true, filePaths: [] };208}209210try {211return await this.getWindowDialogQueue<electron.OpenDialogReturnValue>(window).queue(async () => {212let result: electron.OpenDialogReturnValue;213if (window) {214result = await electron.dialog.showOpenDialog(window, options);215} else {216result = await electron.dialog.showOpenDialog(options);217}218219result.filePaths = this.normalizePaths(result.filePaths);220221return result;222});223} finally {224dispose(fileDialogLock);225}226}227228private acquireFileDialogLock(options: electron.SaveDialogOptions | electron.OpenDialogOptions, window?: electron.BrowserWindow): IDisposable | undefined {229230// If no window is provided, allow as many dialogs as231// needed since we consider them not modal per window232if (!window) {233return Disposable.None;234}235236// If a window is provided, only allow a single dialog237// at the same time because dialogs are modal and we238// do not want to open one dialog after the other239// (https://github.com/microsoft/vscode/issues/114432)240// we figure this out by `hashing` the configuration241// options for the dialog to prevent duplicates242243this.logService.trace('[DialogMainService]: request to acquire file dialog lock', options);244245let windowFileDialogLocks = this.windowFileDialogLocks.get(window.id);246if (!windowFileDialogLocks) {247windowFileDialogLocks = new Set();248this.windowFileDialogLocks.set(window.id, windowFileDialogLocks);249}250251const optionsHash = hash(options);252if (windowFileDialogLocks.has(optionsHash)) {253return undefined; // prevent duplicates, return254}255256this.logService.trace('[DialogMainService]: new file dialog lock created', options);257258windowFileDialogLocks.add(optionsHash);259260return toDisposable(() => {261this.logService.trace('[DialogMainService]: file dialog lock disposed', options);262263windowFileDialogLocks?.delete(optionsHash);264265// If the window has no more dialog locks, delete it from the set of locks266if (windowFileDialogLocks?.size === 0) {267this.windowFileDialogLocks.delete(window.id);268}269});270}271}272273274