/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import nls = require('vs/nls'); import product from 'vs/platform/node/product'; import { TPromise } from 'vs/base/common/winjs.base'; import Severity from 'vs/base/common/severity'; import { isLinux, isMacintosh } from 'vs/base/common/platform'; import { Action } from 'vs/base/common/actions'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IConfirmationService, IChoiceService, IConfirmation, IConfirmationResult, Choice } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService, INotificationHandle, INotificationActions } from 'vs/platform/notification/common/notification'; import { once } from 'vs/base/common/event'; import URI from 'vs/base/common/uri'; import { basename } from 'vs/base/common/paths'; 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[]; } export class DialogService implements IChoiceService, IConfirmationService { public _serviceBrand: any; constructor( @IWindowService private windowService: IWindowService, @INotificationService private notificationService: INotificationService ) { } public confirmWithCheckbox(confirmation: IConfirmation): TPromise { const { options, buttonIndexMap } = this.massageMessageBoxOptions(this.getConfirmOptions(confirmation)); return this.windowService.showMessageBox(options).then(result => { return { confirmed: buttonIndexMap[result.button] === 0 ? true : false, checkboxChecked: result.checkboxChecked } as IConfirmationResult; }); } public confirm(confirmation: IConfirmation): TPromise { const { options, buttonIndexMap } = this.massageMessageBoxOptions(this.getConfirmOptions(confirmation)); return this.windowService.showMessageBox(options).then(result => buttonIndexMap[result.button] === 0 ? true : false); } 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, defaultId: 0, 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; } public choose(severity: Severity, message: string, choices: Choice[], cancelId?: number, modal: boolean = false): TPromise { if (modal) { return this.doChooseWithDialog(severity, message, choices, cancelId); } return this.doChooseWithNotification(severity, message, choices); } private doChooseWithDialog(severity: Severity, message: string, choices: Choice[], cancelId?: number): TPromise { const type: 'none' | 'info' | 'error' | 'question' | 'warning' = severity === Severity.Info ? 'question' : severity === Severity.Error ? 'error' : severity === Severity.Warning ? 'warning' : 'none'; const stringChoices: string[] = []; choices.forEach(choice => { if (typeof choice === 'string') { stringChoices.push(choice); } else { stringChoices.push(choice.label); } }); const { options, buttonIndexMap } = this.massageMessageBoxOptions({ message, buttons: stringChoices, type, cancelId }); return this.windowService.showMessageBox(options).then(result => buttonIndexMap[result.button]); } private doChooseWithNotification(severity: Severity, message: string, choices: Choice[]): TPromise { let handle: INotificationHandle; const promise = new TPromise((c, e) => { // Complete promise with index of action that was picked const callback = (index: number, closeNotification: boolean) => () => { c(index); if (closeNotification) { handle.close(); } return TPromise.as(void 0); }; // Convert choices into primary/secondary actions const actions: INotificationActions = { primary: [], secondary: [] }; choices.forEach((choice, index) => { let isPrimary = true; let label: string; let closeNotification = false; if (typeof choice === 'string') { label = choice; } else { isPrimary = false; label = choice.label; closeNotification = !choice.keepOpen; } const action = new Action(`workbench.dialog.choice.${index}`, label, null, true, callback(index, closeNotification)); if (isPrimary) { actions.primary.push(action); } else { actions.secondary.push(action); } }); // Show notification with actions handle = this.notificationService.notify({ severity, message, actions }); // Cancel promise when notification gets disposed once(handle.onDidClose)(() => promise.cancel()); }, () => handle.close()); return promise; } private massageMessageBoxOptions(options: Electron.MessageBoxOptions): IMassagedMessageBoxOptions { let buttonIndexMap = options.buttons.map((button, index) => index); options.buttons = options.buttons.map(button => mnemonicButtonLabel(button)); // Linux: order of buttons is reverse // macOS: also reverse, but the OS handles this for us! if (isLinux) { options.buttons = options.buttons.reverse(); buttonIndexMap = buttonIndexMap.reverse(); } // Default Button if (options.defaultId !== void 0) { options.defaultId = buttonIndexMap[options.defaultId]; } else if (isLinux) { options.defaultId = buttonIndexMap[0]; } // Cancel Button if (options.cancelId !== void 0) { // macOS: the cancel button should always be to the left of the primary action // if we see more than 2 buttons, move the cancel one to the left of the primary if (isMacintosh && options.buttons.length > 2 && options.cancelId !== 1) { const cancelButton = options.buttons[options.cancelId]; options.buttons.splice(options.cancelId, 1); options.buttons.splice(1, 0, cancelButton); const cancelButtonIndex = buttonIndexMap[options.cancelId]; buttonIndexMap.splice(cancelButtonIndex, 1); buttonIndexMap.splice(1, 0, cancelButtonIndex); } options.cancelId = buttonIndexMap[options.cancelId]; } options.noLink = true; options.title = options.title || product.nameLong; return { options, buttonIndexMap }; } } const MAX_CONFIRM_FILES = 10; export function getConfirmMessage(start: string, resourcesToConfirm: URI[]): string { const message = [start]; message.push(''); message.push(...resourcesToConfirm.slice(0, MAX_CONFIRM_FILES).map(r => basename(r.fsPath))); if (resourcesToConfirm.length > MAX_CONFIRM_FILES) { if (resourcesToConfirm.length - MAX_CONFIRM_FILES === 1) { message.push(nls.localize('moreFile', "...1 additional file not shown")); } else { message.push(nls.localize('moreFiles', "...{0} additional files not shown", resourcesToConfirm.length - MAX_CONFIRM_FILES)); } } message.push(''); return message.join('\n'); }