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(
J
Joao Moreno 已提交
42 43
		@IWindowService private windowService: IWindowService,
		@ILogService private 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',
103 104 105
			cancelId: dialogOptions ? dialogOptions.cancelId : void 0,
			detail: dialogOptions ? dialogOptions.detail : void 0
		});
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 160 161 162 163 164 165
}

export class FileDialogService implements IFileDialogService {

	_serviceBrand: any;

	constructor(
		@IWindowService private windowService: IWindowService,
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
		@IHistoryService private historyService: IHistoryService,
		@IEnvironmentService private environmentService: IEnvironmentService
	) { }

M
Matt Bierner 已提交
166
	public defaultFilePath(schemeFilter: string): URI | undefined {
M
Martin Aeschlimann 已提交
167 168 169 170 171 172 173 174 175 176
		let candidate: URI;

		// Check for last active file first...
		candidate = this.historyService.getLastActiveFile(schemeFilter);

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

M
Matt Bierner 已提交
177
		return candidate && resources.dirname(candidate) || void 0;
M
Martin Aeschlimann 已提交
178 179
	}

M
Matt Bierner 已提交
180
	public defaultFolderPath(schemeFilter: string): URI | undefined {
M
Martin Aeschlimann 已提交
181 182 183 184 185 186 187 188 189 190
		let candidate: URI;

		// Check for last active file root first...
		candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter);

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

M
Matt Bierner 已提交
191
		return candidate && resources.dirname(candidate) || void 0;
M
Martin Aeschlimann 已提交
192 193
	}

M
Matt Bierner 已提交
194
	public defaultWorkspacePath(schemeFilter: string): URI | undefined {
M
Martin Aeschlimann 已提交
195 196

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

		// ...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
			}
		};
	}

J
Johannes Rieken 已提交
218
	public pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<any> {
M
Martin Aeschlimann 已提交
219 220 221 222 223 224 225 226
		let defaultUri = options.defaultUri;
		if (!defaultUri) {
			options.defaultUri = this.defaultFilePath(Schemas.file);
		}
		return this.windowService.pickFileFolderAndOpen(this.toNativeOpenDialogOptions(options));

	}

J
Johannes Rieken 已提交
227
	public pickFileAndOpen(options: IPickAndOpenOptions): Promise<any> {
M
Martin Aeschlimann 已提交
228 229 230 231 232 233 234
		let defaultUri = options.defaultUri;
		if (!defaultUri) {
			options.defaultUri = this.defaultFilePath(Schemas.file);
		}
		return this.windowService.pickFileAndOpen(this.toNativeOpenDialogOptions(options));
	}

J
Johannes Rieken 已提交
235
	public pickFolderAndOpen(options: IPickAndOpenOptions): Promise<any> {
M
Martin Aeschlimann 已提交
236 237 238 239 240 241 242
		let defaultUri = options.defaultUri;
		if (!defaultUri) {
			options.defaultUri = this.defaultFolderPath(Schemas.file);
		}
		return this.windowService.pickFolderAndOpen(this.toNativeOpenDialogOptions(options));
	}

J
Johannes Rieken 已提交
243
	public pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void> {
M
Martin Aeschlimann 已提交
244 245 246 247 248 249 250 251 252 253
		let defaultUri = options.defaultUri;
		if (!defaultUri) {
			options.defaultUri = this.defaultWorkspacePath(Schemas.file);
		}
		return this.windowService.pickWorkspaceAndOpen(this.toNativeOpenDialogOptions(options));
	}

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

M
Matt Bierner 已提交
260
	public showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {
261 262 263 264
		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.'));
		}
M
Martin Aeschlimann 已提交
265 266 267 268 269 270 271 272
		return this.windowService.showSaveDialog(this.toNativeSaveDialogOptions(options)).then(result => {
			if (result) {
				return URI.file(result);
			}
			return void 0;
		});
	}

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

279 280 281 282
		const newOptions: OpenDialogOptions = {
			title: options.title,
			defaultPath: defaultUri && defaultUri.fsPath,
			buttonLabel: options.openLabel,
283
			filters: options.filters,
284 285
			properties: []
		};
M
Matt Bierner 已提交
286
		newOptions.properties!.push('createDirectory');
M
Martin Aeschlimann 已提交
287
		if (options.canSelectFiles) {
M
Matt Bierner 已提交
288
			newOptions.properties!.push('openFile');
M
Martin Aeschlimann 已提交
289 290
		}
		if (options.canSelectFolders) {
M
Matt Bierner 已提交
291
			newOptions.properties!.push('openDirectory');
M
Martin Aeschlimann 已提交
292 293
		}
		if (options.canSelectMany) {
M
Matt Bierner 已提交
294
			newOptions.properties!.push('multiSelections');
M
Martin Aeschlimann 已提交
295
		}
296
		return this.windowService.showOpenDialog(newOptions).then(result => result ? result.map(URI.file) : void 0);
M
Martin Aeschlimann 已提交
297 298 299 300 301
	}
}

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