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
3296 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 } 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
return this.doPick({ ...options, pickFolders: true, title: localize('openFolder', "Open Folder") }, window);
66
}
67
68
pickFile(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined> {
69
return this.doPick({ ...options, pickFiles: true, title: localize('openFile', "Open File") }, window);
70
}
71
72
pickWorkspace(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined> {
73
const title = localize('openWorkspaceTitle', "Open Workspace from File");
74
const buttonLabel = mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open")).withMnemonic;
75
const filters = WORKSPACE_FILTER;
76
77
return this.doPick({ ...options, pickFiles: true, title, filters, buttonLabel }, window);
78
}
79
80
private async doPick(options: IInternalNativeOpenDialogOptions, window?: electron.BrowserWindow): Promise<string[] | undefined> {
81
82
// Ensure dialog options
83
const dialogOptions: electron.OpenDialogOptions = {
84
title: options.title,
85
buttonLabel: options.buttonLabel,
86
filters: options.filters,
87
defaultPath: options.defaultPath
88
};
89
90
// Ensure properties
91
if (typeof options.pickFiles === 'boolean' || typeof options.pickFolders === 'boolean') {
92
dialogOptions.properties = undefined; // let it override based on the booleans
93
94
if (options.pickFiles && options.pickFolders) {
95
dialogOptions.properties = ['multiSelections', 'openDirectory', 'openFile', 'createDirectory'];
96
}
97
}
98
99
if (!dialogOptions.properties) {
100
dialogOptions.properties = ['multiSelections', options.pickFolders ? 'openDirectory' : 'openFile', 'createDirectory'];
101
}
102
103
if (isMacintosh) {
104
dialogOptions.properties.push('treatPackageAsDirectory'); // always drill into .app files
105
}
106
107
// Show Dialog
108
const result = await this.showOpenDialog(dialogOptions, (window || electron.BrowserWindow.getFocusedWindow()) ?? undefined);
109
if (result && result.filePaths && result.filePaths.length > 0) {
110
return result.filePaths;
111
}
112
113
return undefined;
114
}
115
116
private getWindowDialogQueue<T extends electron.MessageBoxReturnValue | electron.SaveDialogReturnValue | electron.OpenDialogReturnValue>(window?: electron.BrowserWindow): Queue<T> {
117
118
// Queue message box requests per window so that one can show
119
// after the other.
120
if (window) {
121
let windowDialogQueue = this.windowDialogQueues.get(window.id);
122
if (!windowDialogQueue) {
123
windowDialogQueue = new Queue<electron.MessageBoxReturnValue | electron.SaveDialogReturnValue | electron.OpenDialogReturnValue>();
124
this.windowDialogQueues.set(window.id, windowDialogQueue);
125
}
126
127
return windowDialogQueue as unknown as Queue<T>;
128
} else {
129
return this.noWindowDialogueQueue as unknown as Queue<T>;
130
}
131
}
132
133
showMessageBox(rawOptions: electron.MessageBoxOptions, window?: electron.BrowserWindow): Promise<electron.MessageBoxReturnValue> {
134
return this.getWindowDialogQueue<electron.MessageBoxReturnValue>(window).queue(async () => {
135
const { options, buttonIndeces } = massageMessageBoxOptions(rawOptions, this.productService);
136
137
let result: electron.MessageBoxReturnValue | undefined = undefined;
138
if (window) {
139
result = await electron.dialog.showMessageBox(window, options);
140
} else {
141
result = await electron.dialog.showMessageBox(options);
142
}
143
144
return {
145
response: buttonIndeces[result.response],
146
checkboxChecked: result.checkboxChecked
147
};
148
});
149
}
150
151
async showSaveDialog(options: electron.SaveDialogOptions, window?: electron.BrowserWindow): Promise<electron.SaveDialogReturnValue> {
152
153
// Prevent duplicates of the same dialog queueing at the same time
154
const fileDialogLock = this.acquireFileDialogLock(options, window);
155
if (!fileDialogLock) {
156
this.logService.error('[DialogMainService]: file save dialog is already or will be showing for the window with the same configuration');
157
158
return { canceled: true, filePath: '' };
159
}
160
161
try {
162
return await this.getWindowDialogQueue<electron.SaveDialogReturnValue>(window).queue(async () => {
163
let result: electron.SaveDialogReturnValue;
164
if (window) {
165
result = await electron.dialog.showSaveDialog(window, options);
166
} else {
167
result = await electron.dialog.showSaveDialog(options);
168
}
169
170
result.filePath = this.normalizePath(result.filePath);
171
172
return result;
173
});
174
} finally {
175
dispose(fileDialogLock);
176
}
177
}
178
179
private normalizePath(path: string): string;
180
private normalizePath(path: string | undefined): string | undefined;
181
private normalizePath(path: string | undefined): string | undefined {
182
if (path && isMacintosh) {
183
path = normalizeNFC(path); // macOS only: normalize paths to NFC form
184
}
185
186
return path;
187
}
188
189
private normalizePaths(paths: string[]): string[] {
190
return paths.map(path => this.normalizePath(path));
191
}
192
193
async showOpenDialog(options: electron.OpenDialogOptions, window?: electron.BrowserWindow): Promise<electron.OpenDialogReturnValue> {
194
195
// Ensure the path exists (if provided)
196
if (options.defaultPath) {
197
const pathExists = await Promises.exists(options.defaultPath);
198
if (!pathExists) {
199
options.defaultPath = undefined;
200
}
201
}
202
203
// Prevent duplicates of the same dialog queueing at the same time
204
const fileDialogLock = this.acquireFileDialogLock(options, window);
205
if (!fileDialogLock) {
206
this.logService.error('[DialogMainService]: file open dialog is already or will be showing for the window with the same configuration');
207
208
return { canceled: true, filePaths: [] };
209
}
210
211
try {
212
return await this.getWindowDialogQueue<electron.OpenDialogReturnValue>(window).queue(async () => {
213
let result: electron.OpenDialogReturnValue;
214
if (window) {
215
result = await electron.dialog.showOpenDialog(window, options);
216
} else {
217
result = await electron.dialog.showOpenDialog(options);
218
}
219
220
result.filePaths = this.normalizePaths(result.filePaths);
221
222
return result;
223
});
224
} finally {
225
dispose(fileDialogLock);
226
}
227
}
228
229
private acquireFileDialogLock(options: electron.SaveDialogOptions | electron.OpenDialogOptions, window?: electron.BrowserWindow): IDisposable | undefined {
230
231
// If no window is provided, allow as many dialogs as
232
// needed since we consider them not modal per window
233
if (!window) {
234
return Disposable.None;
235
}
236
237
// If a window is provided, only allow a single dialog
238
// at the same time because dialogs are modal and we
239
// do not want to open one dialog after the other
240
// (https://github.com/microsoft/vscode/issues/114432)
241
// we figure this out by `hashing` the configuration
242
// options for the dialog to prevent duplicates
243
244
this.logService.trace('[DialogMainService]: request to acquire file dialog lock', options);
245
246
let windowFileDialogLocks = this.windowFileDialogLocks.get(window.id);
247
if (!windowFileDialogLocks) {
248
windowFileDialogLocks = new Set();
249
this.windowFileDialogLocks.set(window.id, windowFileDialogLocks);
250
}
251
252
const optionsHash = hash(options);
253
if (windowFileDialogLocks.has(optionsHash)) {
254
return undefined; // prevent duplicates, return
255
}
256
257
this.logService.trace('[DialogMainService]: new file dialog lock created', options);
258
259
windowFileDialogLocks.add(optionsHash);
260
261
return toDisposable(() => {
262
this.logService.trace('[DialogMainService]: file dialog lock disposed', options);
263
264
windowFileDialogLocks?.delete(optionsHash);
265
266
// If the window has no more dialog locks, delete it from the set of locks
267
if (windowFileDialogLocks?.size === 0) {
268
this.windowFileDialogLocks.delete(window.id);
269
}
270
});
271
}
272
}
273
274