Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/dialogs/electron-main/dialogMainService.ts
5251 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import electron from 'electron';
7
import { Queue } from '../../../base/common/async.js';
8
import { hash } from '../../../base/common/hash.js';
9
import { mnemonicButtonLabel } from '../../../base/common/labels.js';
10
import { Disposable, dispose, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
11
import { normalizeNFC } from '../../../base/common/normalization.js';
12
import { isMacintosh, isWindows } from '../../../base/common/platform.js';
13
import { Promises } from '../../../base/node/pfs.js';
14
import { localize } from '../../../nls.js';
15
import { INativeOpenDialogOptions, massageMessageBoxOptions } from '../common/dialogs.js';
16
import { createDecorator } from '../../instantiation/common/instantiation.js';
17
import { ILogService } from '../../log/common/log.js';
18
import { IProductService } from '../../product/common/productService.js';
19
import { WORKSPACE_FILTER } from '../../workspace/common/workspace.js';
20
21
export const IDialogMainService = createDecorator<IDialogMainService>('dialogMainService');
22
23
export interface IDialogMainService {
24
25
readonly _serviceBrand: undefined;
26
27
pickFileFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined>;
28
pickFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined>;
29
pickFile(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined>;
30
pickWorkspace(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined>;
31
32
showMessageBox(options: electron.MessageBoxOptions, window?: electron.BrowserWindow): Promise<electron.MessageBoxReturnValue>;
33
showSaveDialog(options: electron.SaveDialogOptions, window?: electron.BrowserWindow): Promise<electron.SaveDialogReturnValue>;
34
showOpenDialog(options: electron.OpenDialogOptions, window?: electron.BrowserWindow): Promise<electron.OpenDialogReturnValue>;
35
}
36
37
interface IInternalNativeOpenDialogOptions extends INativeOpenDialogOptions {
38
readonly pickFolders?: boolean;
39
readonly pickFiles?: boolean;
40
41
readonly title: string;
42
readonly buttonLabel?: string;
43
readonly filters?: electron.FileFilter[];
44
}
45
46
export class DialogMainService implements IDialogMainService {
47
48
declare readonly _serviceBrand: undefined;
49
50
private readonly windowFileDialogLocks = new Map<number, Set<number>>();
51
private readonly windowDialogQueues = new Map<number, Queue<electron.MessageBoxReturnValue | electron.SaveDialogReturnValue | electron.OpenDialogReturnValue>>();
52
private readonly noWindowDialogueQueue = new Queue<electron.MessageBoxReturnValue | electron.SaveDialogReturnValue | electron.OpenDialogReturnValue>();
53
54
constructor(
55
@ILogService private readonly logService: ILogService,
56
@IProductService private readonly productService: IProductService
57
) {
58
}
59
60
pickFileFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined> {
61
return this.doPick({ ...options, pickFolders: true, pickFiles: true, title: localize('open', "Open") }, window);
62
}
63
64
pickFolder(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined> {
65
let optionsInternal: IInternalNativeOpenDialogOptions = {
66
...options,
67
pickFolders: true,
68
title: localize('openFolder', "Open Folder")
69
};
70
71
if (isWindows) {
72
// Due to Windows/Electron issue the labels on Open Folder dialog have no hot keys.
73
// We can fix this here for the button label, but some other labels remain inaccessible.
74
// See https://github.com/electron/electron/issues/48631 for more info.
75
optionsInternal = {
76
...optionsInternal,
77
buttonLabel: mnemonicButtonLabel(localize({ key: 'selectFolder', comment: ['&& denotes a mnemonic'] }, "&&Select folder")).withMnemonic
78
};
79
}
80
81
return this.doPick(optionsInternal, window);
82
}
83
84
pickFile(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined> {
85
return this.doPick({ ...options, pickFiles: true, title: localize('openFile', "Open File") }, window);
86
}
87
88
pickWorkspace(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined> {
89
const title = localize('openWorkspaceTitle', "Open Workspace from File");
90
const buttonLabel = mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open")).withMnemonic;
91
const filters = WORKSPACE_FILTER;
92
93
return this.doPick({ ...options, pickFiles: true, title, filters, buttonLabel }, window);
94
}
95
96
private async doPick(options: IInternalNativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined> {
97
98
// Ensure dialog options
99
const dialogOptions: electron.OpenDialogOptions = {
100
title: options.title,
101
buttonLabel: options.buttonLabel,
102
filters: options.filters,
103
defaultPath: options.defaultPath
104
};
105
106
// Ensure properties
107
if (typeof options.pickFiles === 'boolean' || typeof options.pickFolders === 'boolean') {
108
dialogOptions.properties = undefined; // let it override based on the booleans
109
110
if (options.pickFiles && options.pickFolders) {
111
dialogOptions.properties = ['multiSelections', 'openDirectory', 'openFile', 'createDirectory'];
112
}
113
}
114
115
if (!dialogOptions.properties) {
116
dialogOptions.properties = ['multiSelections', options.pickFolders ? 'openDirectory' : 'openFile', 'createDirectory'];
117
}
118
119
if (isMacintosh) {
120
dialogOptions.properties.push('treatPackageAsDirectory'); // always drill into .app files
121
}
122
123
// Show Dialog
124
const result = await this.showOpenDialog(dialogOptions, (window || electron.BrowserWindow.getFocusedWindow()) ?? undefined);
125
if (result?.filePaths && result.filePaths.length > 0) {
126
return result.filePaths;
127
}
128
129
return undefined;
130
}
131
132
private getWindowDialogQueue<T extends electron.MessageBoxReturnValue | electron.SaveDialogReturnValue | electron.OpenDialogReturnValue>(window?: electron.BrowserWindow): Queue<T> {
133
134
// Queue message box requests per window so that one can show
135
// after the other.
136
if (window) {
137
let windowDialogQueue = this.windowDialogQueues.get(window.id);
138
if (!windowDialogQueue) {
139
windowDialogQueue = new Queue<electron.MessageBoxReturnValue | electron.SaveDialogReturnValue | electron.OpenDialogReturnValue>();
140
this.windowDialogQueues.set(window.id, windowDialogQueue);
141
}
142
143
return windowDialogQueue as unknown as Queue<T>;
144
} else {
145
return this.noWindowDialogueQueue as unknown as Queue<T>;
146
}
147
}
148
149
showMessageBox(rawOptions: electron.MessageBoxOptions, window?: electron.BrowserWindow): Promise<electron.MessageBoxReturnValue> {
150
return this.getWindowDialogQueue<electron.MessageBoxReturnValue>(window).queue(async () => {
151
const { options, buttonIndeces } = massageMessageBoxOptions(rawOptions, this.productService);
152
153
let result: electron.MessageBoxReturnValue | undefined = undefined;
154
if (window) {
155
result = await electron.dialog.showMessageBox(window, options);
156
} else {
157
result = await electron.dialog.showMessageBox(options);
158
}
159
160
return {
161
response: buttonIndeces[result.response],
162
checkboxChecked: result.checkboxChecked
163
};
164
});
165
}
166
167
async showSaveDialog(options: electron.SaveDialogOptions, window?: electron.BrowserWindow): Promise<electron.SaveDialogReturnValue> {
168
169
// Prevent duplicates of the same dialog queueing at the same time
170
const fileDialogLock = this.acquireFileDialogLock(options, window);
171
if (!fileDialogLock) {
172
this.logService.error('[DialogMainService]: file save dialog is already or will be showing for the window with the same configuration');
173
174
return { canceled: true, filePath: '' };
175
}
176
177
try {
178
return await this.getWindowDialogQueue<electron.SaveDialogReturnValue>(window).queue(async () => {
179
let result: electron.SaveDialogReturnValue;
180
if (window) {
181
result = await electron.dialog.showSaveDialog(window, options);
182
} else {
183
result = await electron.dialog.showSaveDialog(options);
184
}
185
186
result.filePath = this.normalizePath(result.filePath);
187
188
return result;
189
});
190
} finally {
191
dispose(fileDialogLock);
192
}
193
}
194
195
private normalizePath(path: string): string;
196
private normalizePath(path: string | undefined): string | undefined;
197
private normalizePath(path: string | undefined): string | undefined {
198
if (path && isMacintosh) {
199
path = normalizeNFC(path); // macOS only: normalize paths to NFC form
200
}
201
202
return path;
203
}
204
205
private normalizePaths(paths: string[]): string[] {
206
return paths.map(path => this.normalizePath(path));
207
}
208
209
async showOpenDialog(options: electron.OpenDialogOptions, window?: electron.BrowserWindow): Promise<electron.OpenDialogReturnValue> {
210
211
// Ensure the path exists (if provided)
212
if (options.defaultPath) {
213
const pathExists = await Promises.exists(options.defaultPath);
214
if (!pathExists) {
215
options.defaultPath = undefined;
216
}
217
}
218
219
// Prevent duplicates of the same dialog queueing at the same time
220
const fileDialogLock = this.acquireFileDialogLock(options, window);
221
if (!fileDialogLock) {
222
this.logService.error('[DialogMainService]: file open dialog is already or will be showing for the window with the same configuration');
223
224
return { canceled: true, filePaths: [] };
225
}
226
227
try {
228
return await this.getWindowDialogQueue<electron.OpenDialogReturnValue>(window).queue(async () => {
229
let result: electron.OpenDialogReturnValue;
230
if (window) {
231
result = await electron.dialog.showOpenDialog(window, options);
232
} else {
233
result = await electron.dialog.showOpenDialog(options);
234
}
235
236
result.filePaths = this.normalizePaths(result.filePaths);
237
238
return result;
239
});
240
} finally {
241
dispose(fileDialogLock);
242
}
243
}
244
245
private acquireFileDialogLock(options: electron.SaveDialogOptions | electron.OpenDialogOptions, window?: electron.BrowserWindow): IDisposable | undefined {
246
247
// If no window is provided, allow as many dialogs as
248
// needed since we consider them not modal per window
249
if (!window) {
250
return Disposable.None;
251
}
252
253
// If a window is provided, only allow a single dialog
254
// at the same time because dialogs are modal and we
255
// do not want to open one dialog after the other
256
// (https://github.com/microsoft/vscode/issues/114432)
257
// we figure this out by `hashing` the configuration
258
// options for the dialog to prevent duplicates
259
260
this.logService.trace('[DialogMainService]: request to acquire file dialog lock', options);
261
262
let windowFileDialogLocks = this.windowFileDialogLocks.get(window.id);
263
if (!windowFileDialogLocks) {
264
windowFileDialogLocks = new Set();
265
this.windowFileDialogLocks.set(window.id, windowFileDialogLocks);
266
}
267
268
const optionsHash = hash(options);
269
if (windowFileDialogLocks.has(optionsHash)) {
270
return undefined; // prevent duplicates, return
271
}
272
273
this.logService.trace('[DialogMainService]: new file dialog lock created', options);
274
275
windowFileDialogLocks.add(optionsHash);
276
277
return toDisposable(() => {
278
this.logService.trace('[DialogMainService]: file dialog lock disposed', options);
279
280
windowFileDialogLocks?.delete(optionsHash);
281
282
// If the window has no more dialog locks, delete it from the set of locks
283
if (windowFileDialogLocks?.size === 0) {
284
this.windowFileDialogLocks.delete(window.id);
285
}
286
});
287
}
288
}
289
290