dialogService.ts 13.7 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import * as nls from 'vs/nls';
7
import product from 'vs/platform/node/product';
8
import Severity from 'vs/base/common/severity';
9
import { isLinux, isWindows } from 'vs/base/common/platform';
10
import { IWindowService, INativeOpenDialogOptions, OpenDialogOptions, IURIToOpen, FileFilter } from 'vs/platform/windows/common/windows';
11
import { mnemonicButtonLabel } from 'vs/base/common/labels';
M
Martin Aeschlimann 已提交
12
import { IDialogService, IConfirmation, IConfirmationResult, IDialogOptions, IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
J
Joao Moreno 已提交
13
import { ILogService } from 'vs/platform/log/common/log';
M
Martin Aeschlimann 已提交
14 15 16 17 18 19
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import * as resources from 'vs/base/common/resources';
A
Alex Ross 已提交
20 21
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { RemoteFileDialog } from 'vs/workbench/services/dialogs/electron-browser/remoteFileDialog';
22
import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces';
23
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
24

25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
interface IMassagedMessageBoxOptions {

	/**
	 * OS massaged message box options.
	 */
	options: Electron.MessageBoxOptions;

	/**
	 * Since the massaged result of the message box options potentially
	 * changes the order of buttons, we have to keep a map of these
	 * changes so that we can still return the correct index to the caller.
	 */
	buttonIndexMap: number[];
}

40
export class DialogService implements IDialogService {
41

B
Benjamin Pasero 已提交
42
	_serviceBrand: any;
43 44

	constructor(
45 46
		@IWindowService private readonly windowService: IWindowService,
		@ILogService private readonly logService: ILogService
B
Benjamin Pasero 已提交
47
	) { }
48

J
Johannes Rieken 已提交
49
	confirm(confirmation: IConfirmation): Promise<IConfirmationResult> {
J
Joao Moreno 已提交
50 51
		this.logService.trace('DialogService#confirm', confirmation.message);

52
		const { options, buttonIndexMap } = this.massageMessageBoxOptions(this.getConfirmOptions(confirmation));
53

54
		return this.windowService.showMessageBox(options).then(result => {
55
			return {
56
				confirmed: buttonIndexMap[result.button] === 0 ? true : false,
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
				checkboxChecked: result.checkboxChecked
			} as IConfirmationResult;
		});
	}

	private getConfirmOptions(confirmation: IConfirmation): Electron.MessageBoxOptions {
		const buttons: string[] = [];
		if (confirmation.primaryButton) {
			buttons.push(confirmation.primaryButton);
		} else {
			buttons.push(nls.localize({ key: 'yesButton', comment: ['&& denotes a mnemonic'] }, "&&Yes"));
		}

		if (confirmation.secondaryButton) {
			buttons.push(confirmation.secondaryButton);
		} else if (typeof confirmation.secondaryButton === 'undefined') {
			buttons.push(nls.localize('cancelButton', "Cancel"));
		}

		const opts: Electron.MessageBoxOptions = {
			title: confirmation.title,
			message: confirmation.message,
			buttons,
			cancelId: 1
		};

		if (confirmation.detail) {
			opts.detail = confirmation.detail;
		}

		if (confirmation.type) {
			opts.type = confirmation.type;
		}

		if (confirmation.checkbox) {
			opts.checkboxLabel = confirmation.checkbox.label;
			opts.checkboxChecked = confirmation.checkbox.checked;
		}

		return opts;
	}

J
Johannes Rieken 已提交
99
	show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): Promise<number> {
J
Joao Moreno 已提交
100 101
		this.logService.trace('DialogService#show', message);

102 103 104
		const { options, buttonIndexMap } = this.massageMessageBoxOptions({
			message,
			buttons,
105
			type: (severity === Severity.Info) ? 'question' : (severity === Severity.Error) ? 'error' : (severity === Severity.Warning) ? 'warning' : 'none',
R
Rob Lourens 已提交
106 107
			cancelId: dialogOptions ? dialogOptions.cancelId : undefined,
			detail: dialogOptions ? dialogOptions.detail : undefined
108
		});
109 110

		return this.windowService.showMessageBox(options).then(result => buttonIndexMap[result.button]);
111 112
	}

113
	private massageMessageBoxOptions(options: Electron.MessageBoxOptions): IMassagedMessageBoxOptions {
M
Matt Bierner 已提交
114 115
		let buttonIndexMap = (options.buttons || []).map((button, index) => index);
		let buttons = (options.buttons || []).map(button => mnemonicButtonLabel(button));
116
		let cancelId = options.cancelId;
117

118 119 120
		// Linux: order of buttons is reverse
		// macOS: also reverse, but the OS handles this for us!
		if (isLinux) {
121
			buttons = buttons.reverse();
122 123
			buttonIndexMap = buttonIndexMap.reverse();
		}
124

125 126
		// Default Button (always first one)
		options.defaultId = buttonIndexMap[0];
127

128
		// Cancel Button
129
		if (typeof cancelId === 'number') {
130

131 132 133 134
			// Ensure the cancelId is the correct one from our mapping
			cancelId = buttonIndexMap[cancelId];

			// macOS/Linux: the cancel button should always be to the left of the primary action
135
			// if we see more than 2 buttons, move the cancel one to the left of the primary
136 137 138 139
			if (!isWindows && buttons.length > 2 && cancelId !== 1) {
				const cancelButton = buttons[cancelId];
				buttons.splice(cancelId, 1);
				buttons.splice(1, 0, cancelButton);
140

141 142
				const cancelButtonIndex = buttonIndexMap[cancelId];
				buttonIndexMap.splice(cancelId, 1);
143 144
				buttonIndexMap.splice(1, 0, cancelButtonIndex);

145 146
				cancelId = 1;
			}
147 148
		}

149 150
		options.buttons = buttons;
		options.cancelId = cancelId;
151 152
		options.noLink = true;
		options.title = options.title || product.nameLong;
153

154
		return { options, buttonIndexMap };
155
	}
M
Martin Aeschlimann 已提交
156 157 158 159 160 161 162
}

export class FileDialogService implements IFileDialogService {

	_serviceBrand: any;

	constructor(
163 164 165
		@IWindowService private readonly windowService: IWindowService,
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IHistoryService private readonly historyService: IHistoryService,
A
Alex Ross 已提交
166
		@IEnvironmentService private readonly environmentService: IEnvironmentService,
M
Martin Aeschlimann 已提交
167
		@IInstantiationService private readonly instantiationService: IInstantiationService
M
Martin Aeschlimann 已提交
168 169
	) { }

170
	defaultFilePath(schemeFilter = this.getSchemeFilterForWindow()): URI | undefined {
B
Benjamin Pasero 已提交
171

M
Martin Aeschlimann 已提交
172
		// Check for last active file first...
M
Matt Bierner 已提交
173
		let candidate = this.historyService.getLastActiveFile(schemeFilter);
M
Martin Aeschlimann 已提交
174 175 176 177 178 179

		// ...then for last active file root
		if (!candidate) {
			candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter);
		}

R
Rob Lourens 已提交
180
		return candidate && resources.dirname(candidate) || undefined;
M
Martin Aeschlimann 已提交
181 182
	}

183
	defaultFolderPath(schemeFilter = this.getSchemeFilterForWindow()): URI | undefined {
B
Benjamin Pasero 已提交
184

M
Martin Aeschlimann 已提交
185
		// Check for last active file root first...
M
Matt Bierner 已提交
186
		let candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter);
M
Martin Aeschlimann 已提交
187 188 189 190 191 192

		// ...then for last active file
		if (!candidate) {
			candidate = this.historyService.getLastActiveFile(schemeFilter);
		}

R
Rob Lourens 已提交
193
		return candidate && resources.dirname(candidate) || undefined;
M
Martin Aeschlimann 已提交
194 195
	}

196
	defaultWorkspacePath(schemeFilter = this.getSchemeFilterForWindow()): URI | undefined {
M
Martin Aeschlimann 已提交
197 198

		// Check for current workspace config file first...
199
		if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
M
Matt Bierner 已提交
200
			const configuration = this.contextService.getWorkspace().configuration;
201
			if (configuration && !isUntitledWorkspace(configuration, this.environmentService)) {
R
Rob Lourens 已提交
202
				return resources.dirname(configuration) || undefined;
M
Matt Bierner 已提交
203
			}
M
Martin Aeschlimann 已提交
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
		}

		// ...then fallback to default folder path
		return this.defaultFolderPath(schemeFilter);
	}

	private toNativeOpenDialogOptions(options: IPickAndOpenOptions): INativeOpenDialogOptions {
		return {
			forceNewWindow: options.forceNewWindow,
			telemetryExtraData: options.telemetryExtraData,
			dialogOptions: {
				defaultPath: options.defaultUri && options.defaultUri.fsPath
			}
		};
	}

B
Benjamin Pasero 已提交
220
	pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<any> {
M
Martin Aeschlimann 已提交
221 222 223 224
		const schema = this.getFileSystemSchema(options);

		if (!options.defaultUri) {
			options.defaultUri = this.defaultFilePath(schema);
225 226
		}

M
Martin Aeschlimann 已提交
227
		if (schema !== Schemas.file) {
228
			const title = nls.localize('openFileOrFolder.title', 'Open File Or Folder');
M
Martin Aeschlimann 已提交
229 230
			const availableFileSystems = [schema, Schemas.file]; // always allow file as well
			return this.pickRemoteResourceAndOpen({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }, options.forceNewWindow, true);
M
Martin Aeschlimann 已提交
231 232
		}

B
Benjamin Pasero 已提交
233
		return this.windowService.pickFileFolderAndOpen(this.toNativeOpenDialogOptions(options));
M
Martin Aeschlimann 已提交
234 235
	}

B
Benjamin Pasero 已提交
236
	pickFileAndOpen(options: IPickAndOpenOptions): Promise<any> {
M
Martin Aeschlimann 已提交
237 238 239 240
		const schema = this.getFileSystemSchema(options);

		if (!options.defaultUri) {
			options.defaultUri = this.defaultFilePath(schema);
241 242
		}

M
Martin Aeschlimann 已提交
243
		if (schema !== Schemas.file) {
244
			const title = nls.localize('openFile.title', 'Open File');
M
Martin Aeschlimann 已提交
245 246
			const availableFileSystems = [schema, Schemas.file]; // always allow file as well
			return this.pickRemoteResourceAndOpen({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }, options.forceNewWindow, true);
247 248
		}

M
Martin Aeschlimann 已提交
249 250 251
		return this.windowService.pickFileAndOpen(this.toNativeOpenDialogOptions(options));
	}

B
Benjamin Pasero 已提交
252
	pickFolderAndOpen(options: IPickAndOpenOptions): Promise<any> {
M
Martin Aeschlimann 已提交
253 254 255 256
		const schema = this.getFileSystemSchema(options);

		if (!options.defaultUri) {
			options.defaultUri = this.defaultFolderPath(schema);
257 258
		}

M
Martin Aeschlimann 已提交
259
		if (schema !== Schemas.file) {
260
			const title = nls.localize('openFolder.title', 'Open Folder');
M
Martin Aeschlimann 已提交
261 262
			const availableFileSystems = [schema, Schemas.file]; // always allow file as well
			return this.pickRemoteResourceAndOpen({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }, options.forceNewWindow, false);
263 264
		}

M
Martin Aeschlimann 已提交
265 266 267
		return this.windowService.pickFolderAndOpen(this.toNativeOpenDialogOptions(options));
	}

B
Benjamin Pasero 已提交
268
	pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void> {
M
Martin Aeschlimann 已提交
269 270 271 272
		const schema = this.getFileSystemSchema(options);

		if (!options.defaultUri) {
			options.defaultUri = this.defaultWorkspacePath(schema);
273 274
		}

M
Martin Aeschlimann 已提交
275
		if (schema !== Schemas.file) {
276 277
			const title = nls.localize('openWorkspace.title', 'Open Workspace');
			const filters: FileFilter[] = [{ name: nls.localize('filterName.workspace', 'Workspace'), extensions: [WORKSPACE_EXTENSION] }];
M
Martin Aeschlimann 已提交
278 279 280
			const availableFileSystems = [schema, Schemas.file]; // always allow file as well
			return this.pickRemoteResourceAndOpen({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems }, options.forceNewWindow, false);

281 282
		}

M
Martin Aeschlimann 已提交
283 284 285 286 287 288
		return this.windowService.pickWorkspaceAndOpen(this.toNativeOpenDialogOptions(options));
	}

	private toNativeSaveDialogOptions(options: ISaveDialogOptions): Electron.SaveDialogOptions {
		return {
			defaultPath: options.defaultUri && options.defaultUri.fsPath,
289
			buttonLabel: options.saveLabel,
M
Martin Aeschlimann 已提交
290 291 292 293 294
			filters: options.filters,
			title: options.title
		};
	}

B
Benjamin Pasero 已提交
295
	showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {
M
Martin Aeschlimann 已提交
296 297
		const schema = this.getFileSystemSchema(options);
		if (schema !== Schemas.file) {
298
			return this.saveRemoteResource(options);
299
		}
B
Benjamin Pasero 已提交
300

M
Martin Aeschlimann 已提交
301 302 303 304
		return this.windowService.showSaveDialog(this.toNativeSaveDialogOptions(options)).then(result => {
			if (result) {
				return URI.file(result);
			}
B
Benjamin Pasero 已提交
305

R
Rob Lourens 已提交
306
			return undefined;
M
Martin Aeschlimann 已提交
307 308 309
		});
	}

B
Benjamin Pasero 已提交
310
	showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined> {
M
Martin Aeschlimann 已提交
311 312
		const schema = this.getFileSystemSchema(options);
		if (schema !== Schemas.file) {
313 314 315
			return this.pickRemoteResource(options).then(urisToOpen => {
				return urisToOpen && urisToOpen.map(uto => uto.uri);
			});
316
		}
317

M
Martin Aeschlimann 已提交
318 319
		const defaultUri = options.defaultUri;

320 321 322 323
		const newOptions: OpenDialogOptions = {
			title: options.title,
			defaultPath: defaultUri && defaultUri.fsPath,
			buttonLabel: options.openLabel,
324
			filters: options.filters,
325 326
			properties: []
		};
B
Benjamin Pasero 已提交
327

M
Matt Bierner 已提交
328
		newOptions.properties!.push('createDirectory');
B
Benjamin Pasero 已提交
329

M
Martin Aeschlimann 已提交
330
		if (options.canSelectFiles) {
M
Matt Bierner 已提交
331
			newOptions.properties!.push('openFile');
M
Martin Aeschlimann 已提交
332
		}
B
Benjamin Pasero 已提交
333

M
Martin Aeschlimann 已提交
334
		if (options.canSelectFolders) {
M
Matt Bierner 已提交
335
			newOptions.properties!.push('openDirectory');
M
Martin Aeschlimann 已提交
336
		}
B
Benjamin Pasero 已提交
337

M
Martin Aeschlimann 已提交
338
		if (options.canSelectMany) {
M
Matt Bierner 已提交
339
			newOptions.properties!.push('multiSelections');
M
Martin Aeschlimann 已提交
340
		}
B
Benjamin Pasero 已提交
341

R
Rob Lourens 已提交
342
		return this.windowService.showOpenDialog(newOptions).then(result => result ? result.map(URI.file) : undefined);
M
Martin Aeschlimann 已提交
343
	}
A
Alex Ross 已提交
344

345 346 347 348 349 350 351 352 353 354
	private pickRemoteResourceAndOpen(options: IOpenDialogOptions, forceNewWindow: boolean, forceOpenWorkspaceAsFile: boolean) {
		return this.pickRemoteResource(options).then(urisToOpen => {
			if (urisToOpen) {
				return this.windowService.openWindow(urisToOpen, { forceNewWindow, forceOpenWorkspaceAsFile });
			}
			return void 0;
		});
	}

	private pickRemoteResource(options: IOpenDialogOptions): Promise<IURIToOpen[] | undefined> {
A
Alex Ross 已提交
355 356 357 358
		const remoteFileDialog = this.instantiationService.createInstance(RemoteFileDialog);
		return remoteFileDialog.showOpenDialog(options);
	}

359 360 361
	private saveRemoteResource(options: ISaveDialogOptions): Promise<URI | undefined> {
		const remoteFileDialog = this.instantiationService.createInstance(RemoteFileDialog);
		return remoteFileDialog.showSaveDialog(options);
A
Alex Ross 已提交
362 363
	}

M
Martin Aeschlimann 已提交
364 365 366 367 368 369
	private getSchemeFilterForWindow() {
		return !this.windowService.getConfiguration().remoteAuthority ? Schemas.file : REMOTE_HOST_SCHEME;
	}

	private getFileSystemSchema(options: { availableFileSystems?: string[], defaultUri?: URI }): string {
		return options.availableFileSystems && options.availableFileSystems[0] || options.defaultUri && options.defaultUri.scheme || this.getSchemeFilterForWindow();
A
Alex Ross 已提交
370
	}
371

M
Martin Aeschlimann 已提交
372 373
}

374 375
function isUntitledWorkspace(path: URI, environmentService: IEnvironmentService): boolean {
	return resources.isEqualOrParent(path, environmentService.untitledWorkspacesHome);
376
}