dialogService.ts 10.4 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 } 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 20
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';
import { isParent } from 'vs/platform/files/common/files';
21

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
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[];
}

37
export class DialogService implements IDialogService {
38

B
Benjamin Pasero 已提交
39
	_serviceBrand: any;
40 41

	constructor(
42 43
		@IWindowService private readonly windowService: IWindowService,
		@ILogService private readonly logService: ILogService
B
Benjamin Pasero 已提交
44
	) { }
45

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

49
		const { options, buttonIndexMap } = this.massageMessageBoxOptions(this.getConfirmOptions(confirmation));
50

51
		return this.windowService.showMessageBox(options).then(result => {
52
			return {
53
				confirmed: buttonIndexMap[result.button] === 0 ? true : false,
54 55 56 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
				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 已提交
96
	show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): Promise<number> {
J
Joao Moreno 已提交
97 98
		this.logService.trace('DialogService#show', message);

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

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

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

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

122 123
		// Default Button (always first one)
		options.defaultId = buttonIndexMap[0];
124

125
		// Cancel Button
126
		if (typeof cancelId === 'number') {
127

128 129 130 131
			// 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
132
			// if we see more than 2 buttons, move the cancel one to the left of the primary
133 134 135 136
			if (!isWindows && buttons.length > 2 && cancelId !== 1) {
				const cancelButton = buttons[cancelId];
				buttons.splice(cancelId, 1);
				buttons.splice(1, 0, cancelButton);
137

138 139
				const cancelButtonIndex = buttonIndexMap[cancelId];
				buttonIndexMap.splice(cancelId, 1);
140 141
				buttonIndexMap.splice(1, 0, cancelButtonIndex);

142 143
				cancelId = 1;
			}
144 145
		}

146 147
		options.buttons = buttons;
		options.cancelId = cancelId;
148 149
		options.noLink = true;
		options.title = options.title || product.nameLong;
150

151
		return { options, buttonIndexMap };
152
	}
M
Martin Aeschlimann 已提交
153 154 155 156 157 158 159
}

export class FileDialogService implements IFileDialogService {

	_serviceBrand: any;

	constructor(
160 161 162 163
		@IWindowService private readonly windowService: IWindowService,
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IHistoryService private readonly historyService: IHistoryService,
		@IEnvironmentService private readonly environmentService: IEnvironmentService
M
Martin Aeschlimann 已提交
164 165
	) { }

B
Benjamin Pasero 已提交
166 167
	defaultFilePath(schemeFilter: string): URI | undefined {

M
Martin Aeschlimann 已提交
168
		// Check for last active file first...
M
Matt Bierner 已提交
169
		let candidate = this.historyService.getLastActiveFile(schemeFilter);
M
Martin Aeschlimann 已提交
170 171 172 173 174 175

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

R
Rob Lourens 已提交
176
		return candidate && resources.dirname(candidate) || undefined;
M
Martin Aeschlimann 已提交
177 178
	}

B
Benjamin Pasero 已提交
179 180
	defaultFolderPath(schemeFilter: string): URI | undefined {

M
Martin Aeschlimann 已提交
181
		// Check for last active file root first...
M
Matt Bierner 已提交
182
		let candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter);
M
Martin Aeschlimann 已提交
183 184 185 186 187 188

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

R
Rob Lourens 已提交
189
		return candidate && resources.dirname(candidate) || undefined;
M
Martin Aeschlimann 已提交
190 191
	}

B
Benjamin Pasero 已提交
192
	defaultWorkspacePath(schemeFilter: string): URI | undefined {
M
Martin Aeschlimann 已提交
193 194

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

		// ...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 已提交
216 217
	pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<any> {
		const defaultUri = options.defaultUri;
M
Martin Aeschlimann 已提交
218 219 220 221
		if (!defaultUri) {
			options.defaultUri = this.defaultFilePath(Schemas.file);
		}

B
Benjamin Pasero 已提交
222
		return this.windowService.pickFileFolderAndOpen(this.toNativeOpenDialogOptions(options));
M
Martin Aeschlimann 已提交
223 224
	}

B
Benjamin Pasero 已提交
225 226
	pickFileAndOpen(options: IPickAndOpenOptions): Promise<any> {
		const defaultUri = options.defaultUri;
M
Martin Aeschlimann 已提交
227 228 229
		if (!defaultUri) {
			options.defaultUri = this.defaultFilePath(Schemas.file);
		}
B
Benjamin Pasero 已提交
230

M
Martin Aeschlimann 已提交
231 232 233
		return this.windowService.pickFileAndOpen(this.toNativeOpenDialogOptions(options));
	}

B
Benjamin Pasero 已提交
234 235
	pickFolderAndOpen(options: IPickAndOpenOptions): Promise<any> {
		const defaultUri = options.defaultUri;
M
Martin Aeschlimann 已提交
236 237 238
		if (!defaultUri) {
			options.defaultUri = this.defaultFolderPath(Schemas.file);
		}
B
Benjamin Pasero 已提交
239

M
Martin Aeschlimann 已提交
240 241 242
		return this.windowService.pickFolderAndOpen(this.toNativeOpenDialogOptions(options));
	}

B
Benjamin Pasero 已提交
243 244
	pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void> {
		const defaultUri = options.defaultUri;
M
Martin Aeschlimann 已提交
245 246 247
		if (!defaultUri) {
			options.defaultUri = this.defaultWorkspacePath(Schemas.file);
		}
B
Benjamin Pasero 已提交
248

M
Martin Aeschlimann 已提交
249 250 251 252 253 254
		return this.windowService.pickWorkspaceAndOpen(this.toNativeOpenDialogOptions(options));
	}

	private toNativeSaveDialogOptions(options: ISaveDialogOptions): Electron.SaveDialogOptions {
		return {
			defaultPath: options.defaultUri && options.defaultUri.fsPath,
255
			buttonLabel: options.saveLabel,
M
Martin Aeschlimann 已提交
256 257 258 259 260
			filters: options.filters,
			title: options.title
		};
	}

B
Benjamin Pasero 已提交
261
	showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {
262 263 264 265
		const defaultUri = options.defaultUri;
		if (defaultUri && defaultUri.scheme !== Schemas.file) {
			return Promise.reject(new Error('Not supported - Save-dialogs can only be opened on `file`-uris.'));
		}
B
Benjamin Pasero 已提交
266

M
Martin Aeschlimann 已提交
267 268 269 270
		return this.windowService.showSaveDialog(this.toNativeSaveDialogOptions(options)).then(result => {
			if (result) {
				return URI.file(result);
			}
B
Benjamin Pasero 已提交
271

R
Rob Lourens 已提交
272
			return undefined;
M
Martin Aeschlimann 已提交
273 274 275
		});
	}

B
Benjamin Pasero 已提交
276
	showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined> {
M
Martin Aeschlimann 已提交
277
		const defaultUri = options.defaultUri;
278 279 280
		if (defaultUri && defaultUri.scheme !== Schemas.file) {
			return Promise.reject(new Error('Not supported - Open-dialogs can only be opened on `file`-uris.'));
		}
281

282 283 284 285
		const newOptions: OpenDialogOptions = {
			title: options.title,
			defaultPath: defaultUri && defaultUri.fsPath,
			buttonLabel: options.openLabel,
286
			filters: options.filters,
287 288
			properties: []
		};
B
Benjamin Pasero 已提交
289

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

M
Martin Aeschlimann 已提交
292
		if (options.canSelectFiles) {
M
Matt Bierner 已提交
293
			newOptions.properties!.push('openFile');
M
Martin Aeschlimann 已提交
294
		}
B
Benjamin Pasero 已提交
295

M
Martin Aeschlimann 已提交
296
		if (options.canSelectFolders) {
M
Matt Bierner 已提交
297
			newOptions.properties!.push('openDirectory');
M
Martin Aeschlimann 已提交
298
		}
B
Benjamin Pasero 已提交
299

M
Martin Aeschlimann 已提交
300
		if (options.canSelectMany) {
M
Matt Bierner 已提交
301
			newOptions.properties!.push('multiSelections');
M
Martin Aeschlimann 已提交
302
		}
B
Benjamin Pasero 已提交
303

R
Rob Lourens 已提交
304
		return this.windowService.showOpenDialog(newOptions).then(result => result ? result.map(URI.file) : undefined);
M
Martin Aeschlimann 已提交
305 306 307 308 309
	}
}

function isUntitledWorkspace(path: string, environmentService: IEnvironmentService): boolean {
	return isParent(path, environmentService.workspacesHome, !isLinux /* ignore case */);
310
}