From 384620ca5066c7c20559aa9ad6e4ea8f77f30869 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 23 Jan 2023 13:25:38 +0100 Subject: [PATCH 1/3] dialogs - add a common `massageMessageBoxOptions` that adheres to HIGs --- src/vs/platform/dialogs/common/dialogs.ts | 116 +++++++++++++++ .../dialogs/test/common/dialog.test.ts | 134 ++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 src/vs/platform/dialogs/test/common/dialog.test.ts diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index 076b0b3a3e610..b2e64f0bca719 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -12,6 +12,11 @@ import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; +import { MessageBoxOptions } from 'vs/base/parts/sandbox/common/electronTypes'; +import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { deepClone } from 'vs/base/common/objects'; export interface FileFilter { readonly extensions: string[]; @@ -402,3 +407,114 @@ export interface INativeOpenDialogOptions { readonly telemetryEventName?: string; readonly telemetryExtraData?: ITelemetryData; } + +export interface IMassagedMessageBoxOptions { + + /** + * OS massaged message box options. + */ + options: 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. + */ + buttonIndeces: number[]; +} + +/** + * A utility method to ensure the options for the message box dialog + * are using properties that are consistent across all platforms and + * specific to the platform where necessary. + */ +export function massageMessageBoxOptions(options: MessageBoxOptions, productService: IProductService): IMassagedMessageBoxOptions { + const massagedOptions = deepClone(options); + + let buttons = (massagedOptions.buttons ?? []).map(button => mnemonicButtonLabel(button)); + let buttonIndeces = (options.buttons || []).map((button, index) => index); + + let cancelId = massagedOptions.cancelId; + let defaultId = 0; // by default the first button is default button + + // Apply HIG per OS when more than one button is used + if (buttons.length > 1) { + if (isLinux) { + + // Linux: the GNOME HIG (https://developer.gnome.org/hig/patterns/feedback/dialogs.html?highlight=dialog) + // recommend the following: + // "Always ensure that the cancel button appears first, before the affirmative button. In left-to-right + // locales, this is on the left. This button order ensures that users become aware of, and are reminded + // of, the ability to cancel prior to encountering the affirmative button." + // + // Electron APIs do not reorder buttons for us, so we ensure a reverse order of buttons and a position + // of the cancel button (if provided) that matches the HIG + + const cancelButton = typeof cancelId === 'number' ? buttons[cancelId] : undefined; + + buttons = buttons.reverse(); + buttonIndeces = buttonIndeces.reverse(); + + defaultId = buttons.length - 1; + + if (typeof cancelButton === 'string') { + cancelId = buttons.indexOf(cancelButton); + if (cancelId !== buttons.length - 2 /* left to primary action */) { + buttons.splice(cancelId, 1); + buttons.splice(buttons.length - 1, 0, cancelButton); + + const buttonIndex = buttonIndeces[cancelId]; + buttonIndeces.splice(cancelId, 1); + buttonIndeces.splice(buttonIndeces.length - 1, 0, buttonIndex); + + cancelId = buttons.length - 2; + } + } + } else if (isWindows) { + + // Windows: the HIG (https://learn.microsoft.com/en-us/windows/win32/uxguide/win-dialog-box) + // recommend the following: + // "One of the following sets of concise commands: Yes/No, Yes/No/Cancel, [Do it]/Cancel, + // [Do it]/[Don't do it], [Do it]/[Don't do it]/Cancel." + // + // Electron APIs do not reorder buttons for us, so we ensure the position of the cancel button + // (if provided) that matches the HIG + + const cancelButton = typeof cancelId === 'number' ? buttons[cancelId] : undefined; + + if (typeof cancelButton === 'string') { + cancelId = buttons.indexOf(cancelButton); + if (cancelId !== buttons.length - 1 /* right to primary action */) { + buttons.splice(cancelId, 1); + buttons.push(cancelButton); + + const buttonIndex = buttonIndeces[cancelId]; + buttonIndeces.splice(cancelId, 1); + buttonIndeces.push(buttonIndex); + + cancelId = buttons.length - 1; + } + } + } else if (isMacintosh) { + + // macOS: the HIG (https://developer.apple.com/design/human-interface-guidelines/components/presentation/alerts) + // recommend the following: + // "Place buttons where people expect. In general, place the button people are most likely to choose on the trailing side in a + // row of buttons or at the top in a stack of buttons. Always place the default button on the trailing side of a row or at the + // top of a stack. Cancel buttons are typically on the leading side of a row or at the bottom of a stack." + // + // Electron APIs ensure above for us, so there is nothing we have to do. + } + } + + massagedOptions.buttons = buttons; + massagedOptions.defaultId = defaultId; + massagedOptions.cancelId = cancelId; + massagedOptions.noLink = true; + massagedOptions.title = massagedOptions.title || productService.nameLong; + + return { + options: massagedOptions, + buttonIndeces + }; +} diff --git a/src/vs/platform/dialogs/test/common/dialog.test.ts b/src/vs/platform/dialogs/test/common/dialog.test.ts new file mode 100644 index 0000000000000..8b4fc49640933 --- /dev/null +++ b/src/vs/platform/dialogs/test/common/dialog.test.ts @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepEqual } from 'assert'; +import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { IMassagedMessageBoxOptions, massageMessageBoxOptions } from 'vs/platform/dialogs/common/dialogs'; +import product from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/productService'; + +suite('Dialog', () => { + + function assertOptions({ options, buttonIndeces }: IMassagedMessageBoxOptions, buttons: string[], defaultId: number, cancelId: number | undefined, indeces: number[]): void { + deepEqual(options.buttons, buttons); + deepEqual(options.defaultId, defaultId); + deepEqual(options.cancelId, cancelId); + deepEqual(buttonIndeces, indeces); + } + + test('massageMessageBoxOptions', () => { + const testProductService: IProductService = { + _serviceBrand: undefined, + ...product, + nameLong: 'Test' + }; + + // All platforms + const allPlatformsMassagedOptions = massageMessageBoxOptions({ buttons: ['1'], message: 'message' }, testProductService); + deepEqual(allPlatformsMassagedOptions.options.title, 'Test'); + deepEqual(allPlatformsMassagedOptions.options.message, 'message'); + deepEqual(allPlatformsMassagedOptions.options.noLink, true); + + // Specific cases + + const oneButtonNoCancel = massageMessageBoxOptions({ buttons: ['1'], cancelId: undefined, message: 'message' }, testProductService); + const oneButtonCancel_0 = massageMessageBoxOptions({ buttons: ['1'], cancelId: 0, message: 'message' }, testProductService); + const oneButtonCancel_1 = massageMessageBoxOptions({ buttons: ['1'], cancelId: 1, message: 'message' }, testProductService); + + const twoButtonNoCancel = massageMessageBoxOptions({ buttons: ['1', '2'], cancelId: undefined, message: 'message' }, testProductService); + const twoButtonCancel_0 = massageMessageBoxOptions({ buttons: ['1', '2'], cancelId: 0, message: 'message' }, testProductService); + const twoButtonCancel_1 = massageMessageBoxOptions({ buttons: ['1', '2'], cancelId: 1, message: 'message' }, testProductService); + const twoButtonCancel_2 = massageMessageBoxOptions({ buttons: ['1', '2'], cancelId: 2, message: 'message' }, testProductService); + + const threeButtonNoCancel = massageMessageBoxOptions({ buttons: ['1', '2', '3'], cancelId: undefined, message: 'message' }, testProductService); + const threeButtonCancel_0 = massageMessageBoxOptions({ buttons: ['1', '2', '3'], cancelId: 0, message: 'message' }, testProductService); + const threeButtonCancel_1 = massageMessageBoxOptions({ buttons: ['1', '2', '3'], cancelId: 1, message: 'message' }, testProductService); + const threeButtonCancel_2 = massageMessageBoxOptions({ buttons: ['1', '2', '3'], cancelId: 2, message: 'message' }, testProductService); + const threeButtonCancel_3 = massageMessageBoxOptions({ buttons: ['1', '2', '3'], cancelId: 3, message: 'message' }, testProductService); + + const fourButtonNoCancel = massageMessageBoxOptions({ buttons: ['1', '2', '3', '4'], cancelId: undefined, message: 'message' }, testProductService); + const fourButtonCancel_0 = massageMessageBoxOptions({ buttons: ['1', '2', '3', '4'], cancelId: 0, message: 'message' }, testProductService); + const fourButtonCancel_1 = massageMessageBoxOptions({ buttons: ['1', '2', '3', '4'], cancelId: 1, message: 'message' }, testProductService); + const fourButtonCancel_2 = massageMessageBoxOptions({ buttons: ['1', '2', '3', '4'], cancelId: 2, message: 'message' }, testProductService); + const fourButtonCancel_3 = massageMessageBoxOptions({ buttons: ['1', '2', '3', '4'], cancelId: 3, message: 'message' }, testProductService); + const fourButtonCancel_4 = massageMessageBoxOptions({ buttons: ['1', '2', '3', '4'], cancelId: 4, message: 'message' }, testProductService); + + // Windows: cancel button is always at the end + if (isWindows) { + assertOptions(oneButtonNoCancel, ['1'], 0, undefined, [0]); + assertOptions(oneButtonCancel_0, ['1'], 0, 0, [0]); + assertOptions(oneButtonCancel_1, ['1'], 0, 1, [0]); + + assertOptions(twoButtonNoCancel, ['1', '2'], 0, undefined, [0, 1]); + assertOptions(twoButtonCancel_0, ['2', '1'], 0, 1, [1, 0]); + assertOptions(twoButtonCancel_1, ['1', '2'], 0, 1, [0, 1]); + assertOptions(twoButtonCancel_2, ['1', '2'], 0, 2, [0, 1]); + + assertOptions(threeButtonNoCancel, ['1', '2', '3'], 0, undefined, [0, 1, 2]); + assertOptions(threeButtonCancel_0, ['2', '3', '1'], 0, 2, [1, 2, 0]); + assertOptions(threeButtonCancel_1, ['1', '3', '2'], 0, 2, [0, 2, 1]); + assertOptions(threeButtonCancel_2, ['1', '2', '3'], 0, 2, [0, 1, 2]); + assertOptions(threeButtonCancel_3, ['1', '2', '3'], 0, 3, [0, 1, 2]); + + assertOptions(fourButtonNoCancel, ['1', '2', '3', '4'], 0, undefined, [0, 1, 2, 3]); + assertOptions(fourButtonCancel_0, ['2', '3', '4', '1'], 0, 3, [1, 2, 3, 0]); + assertOptions(fourButtonCancel_1, ['1', '3', '4', '2'], 0, 3, [0, 2, 3, 1]); + assertOptions(fourButtonCancel_2, ['1', '2', '4', '3'], 0, 3, [0, 1, 3, 2]); + assertOptions(fourButtonCancel_3, ['1', '2', '3', '4'], 0, 3, [0, 1, 2, 3]); + assertOptions(fourButtonCancel_4, ['1', '2', '3', '4'], 0, 4, [0, 1, 2, 3]); + } + + // macOS: returns buttons as is + else if (isMacintosh) { + assertOptions(oneButtonNoCancel, ['1'], 0, undefined, [0]); + assertOptions(oneButtonCancel_0, ['1'], 0, 0, [0]); + assertOptions(oneButtonCancel_1, ['1'], 0, 1, [0]); + + assertOptions(twoButtonNoCancel, ['1', '2'], 0, undefined, [0, 1]); + assertOptions(twoButtonCancel_0, ['1', '2'], 0, 0, [0, 1]); + assertOptions(twoButtonCancel_1, ['1', '2'], 0, 1, [0, 1]); + assertOptions(twoButtonCancel_2, ['1', '2'], 0, 2, [0, 1]); + + assertOptions(threeButtonNoCancel, ['1', '2', '3'], 0, undefined, [0, 1, 2]); + assertOptions(threeButtonCancel_0, ['1', '2', '3'], 0, 0, [0, 1, 2]); + assertOptions(threeButtonCancel_1, ['1', '2', '3'], 0, 1, [0, 1, 2]); + assertOptions(threeButtonCancel_2, ['1', '2', '3'], 0, 2, [0, 1, 2]); + assertOptions(threeButtonCancel_3, ['1', '2', '3'], 0, 3, [0, 1, 2]); + + assertOptions(fourButtonNoCancel, ['1', '2', '3', '4'], 0, undefined, [0, 1, 2, 3]); + assertOptions(fourButtonCancel_0, ['1', '2', '3', '4'], 0, 0, [0, 1, 2, 3]); + assertOptions(fourButtonCancel_1, ['1', '2', '3', '4'], 0, 1, [0, 1, 2, 3]); + assertOptions(fourButtonCancel_2, ['1', '2', '3', '4'], 0, 2, [0, 1, 2, 3]); + assertOptions(fourButtonCancel_3, ['1', '2', '3', '4'], 0, 3, [0, 1, 2, 3]); + assertOptions(fourButtonCancel_4, ['1', '2', '3', '4'], 0, 4, [0, 1, 2, 3]); + } + + // Linux: reverses buttons and puts cancel button + // after the first button + else if (isLinux) { + assertOptions(oneButtonNoCancel, ['1'], 0, undefined, [0]); + assertOptions(oneButtonCancel_0, ['1'], 0, 0, [0]); + assertOptions(oneButtonCancel_1, ['1'], 0, 1, [0]); + + assertOptions(twoButtonNoCancel, ['2', '1'], 1, undefined, [1, 0]); + assertOptions(twoButtonCancel_0, ['1', '2'], 1, 0, [0, 1]); + assertOptions(twoButtonCancel_1, ['2', '1'], 1, 0, [1, 0]); + assertOptions(twoButtonCancel_2, ['2', '1'], 1, 2, [1, 0]); + + assertOptions(threeButtonNoCancel, ['3', '2', '1'], 2, undefined, [2, 1, 0]); + assertOptions(threeButtonCancel_0, ['3', '1', '2'], 2, 1, [2, 0, 1]); + assertOptions(threeButtonCancel_1, ['3', '2', '1'], 2, 1, [2, 1, 0]); + assertOptions(threeButtonCancel_2, ['2', '3', '1'], 2, 1, [1, 2, 0]); + assertOptions(threeButtonCancel_3, ['3', '2', '1'], 2, 3, [2, 1, 0]); + + assertOptions(fourButtonNoCancel, ['4', '3', '2', '1'], 3, undefined, [3, 2, 1, 0]); + assertOptions(fourButtonCancel_0, ['4', '3', '1', '2'], 3, 2, [3, 2, 0, 1]); + assertOptions(fourButtonCancel_1, ['4', '3', '2', '1'], 3, 2, [3, 2, 1, 0]); + assertOptions(fourButtonCancel_2, ['4', '2', '3', '1'], 3, 2, [3, 1, 2, 0]); + assertOptions(fourButtonCancel_3, ['3', '2', '4', '1'], 3, 2, [2, 1, 3, 0]); + assertOptions(fourButtonCancel_4, ['4', '3', '2', '1'], 3, 4, [3, 2, 1, 0]); + } + }); +}); From 239fb5f7255930611470f0be0ba7fbdb041e71f4 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 23 Jan 2023 13:43:10 +0100 Subject: [PATCH 2/3] dialogs - set `cancelId` to last button by default --- src/vs/platform/dialogs/common/dialogs.ts | 2 +- .../dialogs/test/common/dialog.test.ts | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index b2e64f0bca719..2732f7e531046 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -434,8 +434,8 @@ export function massageMessageBoxOptions(options: MessageBoxOptions, productServ let buttons = (massagedOptions.buttons ?? []).map(button => mnemonicButtonLabel(button)); let buttonIndeces = (options.buttons || []).map((button, index) => index); - let cancelId = massagedOptions.cancelId; let defaultId = 0; // by default the first button is default button + let cancelId = massagedOptions.cancelId ?? buttons.length - 1; // by default the last button is cancel button // Apply HIG per OS when more than one button is used if (buttons.length > 1) { diff --git a/src/vs/platform/dialogs/test/common/dialog.test.ts b/src/vs/platform/dialogs/test/common/dialog.test.ts index 8b4fc49640933..cceadcb1f47da 100644 --- a/src/vs/platform/dialogs/test/common/dialog.test.ts +++ b/src/vs/platform/dialogs/test/common/dialog.test.ts @@ -11,7 +11,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; suite('Dialog', () => { - function assertOptions({ options, buttonIndeces }: IMassagedMessageBoxOptions, buttons: string[], defaultId: number, cancelId: number | undefined, indeces: number[]): void { + function assertOptions({ options, buttonIndeces }: IMassagedMessageBoxOptions, buttons: string[], defaultId: number, cancelId: number, indeces: number[]): void { deepEqual(options.buttons, buttons); deepEqual(options.defaultId, defaultId); deepEqual(options.cancelId, cancelId); @@ -57,22 +57,22 @@ suite('Dialog', () => { // Windows: cancel button is always at the end if (isWindows) { - assertOptions(oneButtonNoCancel, ['1'], 0, undefined, [0]); + assertOptions(oneButtonNoCancel, ['1'], 0, 0, [0]); assertOptions(oneButtonCancel_0, ['1'], 0, 0, [0]); assertOptions(oneButtonCancel_1, ['1'], 0, 1, [0]); - assertOptions(twoButtonNoCancel, ['1', '2'], 0, undefined, [0, 1]); + assertOptions(twoButtonNoCancel, ['1', '2'], 0, 1, [0, 1]); assertOptions(twoButtonCancel_0, ['2', '1'], 0, 1, [1, 0]); assertOptions(twoButtonCancel_1, ['1', '2'], 0, 1, [0, 1]); assertOptions(twoButtonCancel_2, ['1', '2'], 0, 2, [0, 1]); - assertOptions(threeButtonNoCancel, ['1', '2', '3'], 0, undefined, [0, 1, 2]); + assertOptions(threeButtonNoCancel, ['1', '2', '3'], 0, 2, [0, 1, 2]); assertOptions(threeButtonCancel_0, ['2', '3', '1'], 0, 2, [1, 2, 0]); assertOptions(threeButtonCancel_1, ['1', '3', '2'], 0, 2, [0, 2, 1]); assertOptions(threeButtonCancel_2, ['1', '2', '3'], 0, 2, [0, 1, 2]); assertOptions(threeButtonCancel_3, ['1', '2', '3'], 0, 3, [0, 1, 2]); - assertOptions(fourButtonNoCancel, ['1', '2', '3', '4'], 0, undefined, [0, 1, 2, 3]); + assertOptions(fourButtonNoCancel, ['1', '2', '3', '4'], 0, 3, [0, 1, 2, 3]); assertOptions(fourButtonCancel_0, ['2', '3', '4', '1'], 0, 3, [1, 2, 3, 0]); assertOptions(fourButtonCancel_1, ['1', '3', '4', '2'], 0, 3, [0, 2, 3, 1]); assertOptions(fourButtonCancel_2, ['1', '2', '4', '3'], 0, 3, [0, 1, 3, 2]); @@ -82,22 +82,22 @@ suite('Dialog', () => { // macOS: returns buttons as is else if (isMacintosh) { - assertOptions(oneButtonNoCancel, ['1'], 0, undefined, [0]); + assertOptions(oneButtonNoCancel, ['1'], 0, 0, [0]); assertOptions(oneButtonCancel_0, ['1'], 0, 0, [0]); assertOptions(oneButtonCancel_1, ['1'], 0, 1, [0]); - assertOptions(twoButtonNoCancel, ['1', '2'], 0, undefined, [0, 1]); + assertOptions(twoButtonNoCancel, ['1', '2'], 0, 1, [0, 1]); assertOptions(twoButtonCancel_0, ['1', '2'], 0, 0, [0, 1]); assertOptions(twoButtonCancel_1, ['1', '2'], 0, 1, [0, 1]); assertOptions(twoButtonCancel_2, ['1', '2'], 0, 2, [0, 1]); - assertOptions(threeButtonNoCancel, ['1', '2', '3'], 0, undefined, [0, 1, 2]); + assertOptions(threeButtonNoCancel, ['1', '2', '3'], 0, 2, [0, 1, 2]); assertOptions(threeButtonCancel_0, ['1', '2', '3'], 0, 0, [0, 1, 2]); assertOptions(threeButtonCancel_1, ['1', '2', '3'], 0, 1, [0, 1, 2]); assertOptions(threeButtonCancel_2, ['1', '2', '3'], 0, 2, [0, 1, 2]); assertOptions(threeButtonCancel_3, ['1', '2', '3'], 0, 3, [0, 1, 2]); - assertOptions(fourButtonNoCancel, ['1', '2', '3', '4'], 0, undefined, [0, 1, 2, 3]); + assertOptions(fourButtonNoCancel, ['1', '2', '3', '4'], 0, 3, [0, 1, 2, 3]); assertOptions(fourButtonCancel_0, ['1', '2', '3', '4'], 0, 0, [0, 1, 2, 3]); assertOptions(fourButtonCancel_1, ['1', '2', '3', '4'], 0, 1, [0, 1, 2, 3]); assertOptions(fourButtonCancel_2, ['1', '2', '3', '4'], 0, 2, [0, 1, 2, 3]); @@ -108,22 +108,22 @@ suite('Dialog', () => { // Linux: reverses buttons and puts cancel button // after the first button else if (isLinux) { - assertOptions(oneButtonNoCancel, ['1'], 0, undefined, [0]); + assertOptions(oneButtonNoCancel, ['1'], 0, 0, [0]); assertOptions(oneButtonCancel_0, ['1'], 0, 0, [0]); assertOptions(oneButtonCancel_1, ['1'], 0, 1, [0]); - assertOptions(twoButtonNoCancel, ['2', '1'], 1, undefined, [1, 0]); + assertOptions(twoButtonNoCancel, ['2', '1'], 1, 0, [1, 0]); assertOptions(twoButtonCancel_0, ['1', '2'], 1, 0, [0, 1]); assertOptions(twoButtonCancel_1, ['2', '1'], 1, 0, [1, 0]); assertOptions(twoButtonCancel_2, ['2', '1'], 1, 2, [1, 0]); - assertOptions(threeButtonNoCancel, ['3', '2', '1'], 2, undefined, [2, 1, 0]); + assertOptions(threeButtonNoCancel, ['2', '3', '1'], 2, 1, [1, 2, 0]); assertOptions(threeButtonCancel_0, ['3', '1', '2'], 2, 1, [2, 0, 1]); assertOptions(threeButtonCancel_1, ['3', '2', '1'], 2, 1, [2, 1, 0]); assertOptions(threeButtonCancel_2, ['2', '3', '1'], 2, 1, [1, 2, 0]); assertOptions(threeButtonCancel_3, ['3', '2', '1'], 2, 3, [2, 1, 0]); - assertOptions(fourButtonNoCancel, ['4', '3', '2', '1'], 3, undefined, [3, 2, 1, 0]); + assertOptions(fourButtonNoCancel, ['3', '2', '4', '1'], 3, 2, [2, 1, 3, 0]); assertOptions(fourButtonCancel_0, ['4', '3', '1', '2'], 3, 2, [3, 2, 0, 1]); assertOptions(fourButtonCancel_1, ['4', '3', '2', '1'], 3, 2, [3, 2, 1, 0]); assertOptions(fourButtonCancel_2, ['4', '2', '3', '1'], 3, 2, [3, 1, 2, 0]); From cefbbf94ec7ff08ed629a83e23da61e1665fd4bd Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 23 Jan 2023 14:12:47 +0100 Subject: [PATCH 3/3] dialogs - adopt `massageMessageBoxOptions` for `electron-main` dialog usages --- src/vs/code/electron-main/app.ts | 16 ++--- src/vs/code/electron-main/main.ts | 30 ++++----- .../issue/electron-main/issueMainService.ts | 65 ++++++++----------- .../platform/menubar/electron-main/menubar.ts | 21 +++--- .../electron-main/nativeHostMainService.ts | 39 +++++------ .../windows/electron-main/windowImpl.ts | 56 +++++++--------- .../electron-main/windowsMainService.ts | 18 ++--- .../workspacesManagementMainService.ts | 17 ++--- 8 files changed, 113 insertions(+), 149 deletions(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index cf7eb7847c703..702c372302a0b 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -12,7 +12,7 @@ import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/err import { isEqualOrParent } from 'vs/base/common/extpath'; import { once } from 'vs/base/common/functional'; import { stripComments } from 'vs/base/common/json'; -import { getPathLabel, mnemonicButtonLabel } from 'vs/base/common/labels'; +import { getPathLabel } from 'vs/base/common/labels'; import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isAbsolute, join, posix } from 'vs/base/common/path'; @@ -112,6 +112,7 @@ import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement import { LoggerChannel } from 'vs/platform/log/electron-main/logIpc'; import { ILoggerMainService } from 'vs/platform/log/electron-main/loggerService'; import { IInitialProtocolUrls, IProtocolUrl } from 'vs/platform/url/electron-main/url'; +import { massageMessageBoxOptions } from 'vs/platform/dialogs/common/dialogs'; /** * The main VS Code application. There will only ever be one instance, @@ -664,20 +665,17 @@ export class CodeApplication extends Disposable { private shouldBlockURI(uri: URI): boolean { if (uri.authority === Schemas.file && isWindows) { - const res = dialog.showMessageBoxSync({ - title: this.productService.nameLong, + const { options, buttonIndeces } = massageMessageBoxOptions({ type: 'question', buttons: [ - mnemonicButtonLabel(localize({ key: 'open', comment: ['&& denotes a mnemonic'] }, "&&Yes")), - mnemonicButtonLabel(localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&No")), + localize({ key: 'open', comment: ['&& denotes a mnemonic'] }, "&&Yes"), + localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&No") ], - defaultId: 0, - cancelId: 1, message: localize('confirmOpenMessage', "An external application wants to open '{0}' in {1}. Do you want to open this file or folder?", getPathLabel(uri, { os: OS, tildify: this.environmentMainService }), this.productService.nameShort), detail: localize('confirmOpenDetail', "If you did not initiate this request, it may represent an attempted attack on your system. Unless you took an explicit action to initiate this request, you should press 'No'"), - noLink: true - }); + }, this.productService); + const res = buttonIndeces[dialog.showMessageBoxSync(options)]; if (res === 1) { return true; } diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 38baa9e97119a..7c0c0d28adbe8 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -14,7 +14,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ExpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { IPathWithLineAndColumn, isValidBasename, parseLineAndColumnAware, sanitizeFilePath } from 'vs/base/common/extpath'; import { once } from 'vs/base/common/functional'; -import { getPathLabel, mnemonicButtonLabel } from 'vs/base/common/labels'; +import { getPathLabel } from 'vs/base/common/labels'; import { Schemas } from 'vs/base/common/network'; import { basename, join, resolve } from 'vs/base/common/path'; import { mark } from 'vs/base/common/performance'; @@ -69,6 +69,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { ILoggerMainService, LoggerMainService } from 'vs/platform/log/electron-main/loggerService'; import { LogService } from 'vs/platform/log/common/logService'; +import { massageMessageBoxOptions } from 'vs/platform/dialogs/common/dialogs'; /** * The main VS Code entry point. @@ -106,7 +107,7 @@ class CodeMain { } catch (error) { // Show a dialog for errors that can be resolved by the user - this.handleStartupDataDirError(environmentMainService, productService.nameLong, error); + this.handleStartupDataDirError(environmentMainService, productService, error); throw error; } @@ -279,7 +280,7 @@ class CodeMain { if (error.code !== 'EADDRINUSE') { // Show a dialog for errors that can be resolved by the user - this.handleStartupDataDirError(environmentMainService, productService.nameLong, error); + this.handleStartupDataDirError(environmentMainService, productService, error); // Any other runtime error is just printed to the console throw error; @@ -297,7 +298,7 @@ class CodeMain { this.showStartupWarningDialog( localize('secondInstanceAdmin', "Another instance of {0} is already running as administrator.", productService.nameShort), localize('secondInstanceAdminDetail', "Please close the other instance and try again."), - productService.nameLong + productService ); } @@ -336,7 +337,7 @@ class CodeMain { this.showStartupWarningDialog( localize('secondInstanceNoResponse', "Another instance of {0} is running but not responding", productService.nameShort), localize('secondInstanceNoResponseDetail', "Please close all other instances and try again."), - productService.nameLong + productService ); }, 10000); } @@ -391,31 +392,30 @@ class CodeMain { return mainProcessNodeIpcServer; } - private handleStartupDataDirError(environmentMainService: IEnvironmentMainService, title: string, error: NodeJS.ErrnoException): void { + private handleStartupDataDirError(environmentMainService: IEnvironmentMainService, productService: IProductService, error: NodeJS.ErrnoException): void { if (error.code === 'EACCES' || error.code === 'EPERM') { const directories = coalesce([environmentMainService.userDataPath, environmentMainService.extensionsPath, XDG_RUNTIME_DIR]).map(folder => getPathLabel(URI.file(folder), { os: OS, tildify: environmentMainService })); this.showStartupWarningDialog( localize('startupDataDirError', "Unable to write program user data."), localize('startupUserDataAndExtensionsDirErrorDetail', "{0}\n\nPlease make sure the following directories are writeable:\n\n{1}", toErrorMessage(error), directories.join('\n')), - title + productService ); } } - private showStartupWarningDialog(message: string, detail: string, title: string): void { + private showStartupWarningDialog(message: string, detail: string, productService: IProductService): void { + // use sync variant here because we likely exit after this method // due to startup issues and otherwise the dialog seems to disappear // https://github.com/microsoft/vscode/issues/104493 - dialog.showMessageBoxSync({ - title, + + dialog.showMessageBoxSync(massageMessageBoxOptions({ type: 'warning', - buttons: [mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], + buttons: [localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close")], message, - detail, - defaultId: 0, - noLink: true - }); + detail + }, productService).options); } private async windowsAllowSetForegroundWindow(launchMainService: ILaunchMainService, logService: ILogService): Promise { diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index 36cce3d7a05de..866c8c3c92208 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -6,7 +6,6 @@ import { BrowserWindow, BrowserWindowConstructorOptions, contentTracing, Display, IpcMainEvent, screen } from 'electron'; import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { arch, release, type } from 'os'; -import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; @@ -28,6 +27,7 @@ import { IWindowState } from 'vs/platform/window/electron-main/window'; import { randomPath } from 'vs/base/common/extpath'; import { withNullAsUndefined } from 'vs/base/common/types'; import { IStateMainService } from 'vs/platform/state/electron-main/state'; +import { massageMessageBoxOptions } from 'vs/platform/dialogs/common/dialogs'; @@ -112,22 +112,18 @@ export class IssueMainService implements IIssueMainService { }); validatedIpcMain.on('vscode:issueReporterClipboard', async event => { - const messageOptions = { - title: this.productService.nameLong, - message: localize('issueReporterWriteToClipboard', "There is too much data to send to GitHub directly. The data will be copied to the clipboard, please paste it into the GitHub issue page that is opened."), - type: 'warning', - buttons: [ - mnemonicButtonLabel(localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK")), - mnemonicButtonLabel(localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&Cancel")), - ], - defaultId: 0, - cancelId: 1, - noLink: true - }; - if (this.issueReporterWindow) { - const result = await this.dialogMainService.showMessageBox(messageOptions, this.issueReporterWindow); - this.safeSend(event, 'vscode:issueReporterClipboardResponse', result.response === 0); + const { options, buttonIndeces } = massageMessageBoxOptions({ + message: localize('issueReporterWriteToClipboard', "There is too much data to send to GitHub directly. The data will be copied to the clipboard, please paste it into the GitHub issue page that is opened."), + type: 'warning', + buttons: [ + localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK"), + localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&Cancel") + ] + }, this.productService); + + const result = await this.dialogMainService.showMessageBox(options, this.issueReporterWindow); + this.safeSend(event, 'vscode:issueReporterClipboardResponse', buttonIndeces[result.response] === 0); } }); @@ -137,22 +133,18 @@ export class IssueMainService implements IIssueMainService { }); validatedIpcMain.on('vscode:issueReporterConfirmClose', async () => { - const messageOptions = { - title: this.productService.nameLong, - message: localize('confirmCloseIssueReporter', "Your input will not be saved. Are you sure you want to close this window?"), - type: 'warning', - buttons: [ - mnemonicButtonLabel(localize({ key: 'yes', comment: ['&& denotes a mnemonic'] }, "&&Yes")), - mnemonicButtonLabel(localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&Cancel")), - ], - defaultId: 0, - cancelId: 1, - noLink: true - }; - if (this.issueReporterWindow) { - const result = await this.dialogMainService.showMessageBox(messageOptions, this.issueReporterWindow); - if (result.response === 0) { + const { options, buttonIndeces } = massageMessageBoxOptions({ + message: localize('confirmCloseIssueReporter', "Your input will not be saved. Are you sure you want to close this window?"), + type: 'warning', + buttons: [ + localize({ key: 'yes', comment: ['&& denotes a mnemonic'] }, "&&Yes"), + localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&Cancel") + ] + }, this.productService); + + const result = await this.dialogMainService.showMessageBox(options, this.issueReporterWindow); + if (buttonIndeces[result.response] === 0) { if (this.issueReporterWindow) { this.issueReporterWindow.destroy(); this.issueReporterWindow = null; @@ -472,15 +464,14 @@ export class IssueMainService implements IIssueMainService { const path = await contentTracing.stopRecording(`${randomPath(this.environmentMainService.userHome.fsPath, this.productService.applicationName)}.trace.txt`); // Inform user to report an issue - await this.dialogMainService.showMessageBox({ - title: this.productService.nameLong, + const { options } = massageMessageBoxOptions({ type: 'info', message: localize('trace.message', "Successfully created the trace file"), detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path), - buttons: [mnemonicButtonLabel(localize({ key: 'trace.ok', comment: ['&& denotes a mnemonic'] }, "&&OK"))], - defaultId: 0, - noLink: true - }, withNullAsUndefined(BrowserWindow.getFocusedWindow())); + buttons: [localize({ key: 'trace.ok', comment: ['&& denotes a mnemonic'] }, "&&OK")], + }, this.productService); + + await this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); // Show item in explorer this.nativeHostMainService.showItemInFolder(undefined, path); diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index b61c05ea57429..9f9bdd84f09eb 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, BrowserWindow, KeyboardEvent, Menu, MenuItem, MenuItemConstructorOptions, MessageBoxOptions, WebContents } from 'electron'; +import { app, BrowserWindow, KeyboardEvent, Menu, MenuItem, MenuItemConstructorOptions, WebContents } from 'electron'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { mnemonicButtonLabel, mnemonicMenuLabel } from 'vs/base/common/labels'; +import { mnemonicMenuLabel } from 'vs/base/common/labels'; import { isMacintosh, language } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { massageMessageBoxOptions } from 'vs/platform/dialogs/common/dialogs'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -430,21 +431,17 @@ export class Menubar { const confirmBeforeClose = this.configurationService.getValue<'always' | 'never' | 'keyboardOnly'>('window.confirmBeforeClose'); if (confirmBeforeClose === 'always' || (confirmBeforeClose === 'keyboardOnly' && this.isKeyboardEvent(event))) { - const options: MessageBoxOptions = { - title: this.productService.nameLong, + const { options, buttonIndeces } = massageMessageBoxOptions({ type: 'question', buttons: [ - mnemonicButtonLabel(nls.localize({ key: 'quit', comment: ['&& denotes a mnemonic'] }, "&&Quit")), - mnemonicButtonLabel(nls.localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&Cancel")) + nls.localize({ key: 'quit', comment: ['&& denotes a mnemonic'] }, "&&Quit"), + nls.localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&Cancel") ], - message: nls.localize('quitMessage', "Are you sure you want to quit?"), - noLink: true, - defaultId: 0, - cancelId: 1 - }; + message: nls.localize('quitMessage', "Are you sure you want to quit?") + }, this.productService); const { response } = await this.nativeHostMainService.showMessageBox(this.windowsMainService.getFocusedWindow()?.id, options); - return response === 0; + return buttonIndeces[response] === 0; } return true; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 0397052b05f76..edca8ee06f204 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -9,7 +9,6 @@ import { arch, cpus, freemem, loadavg, platform, release, totalmem, type } from import { promisify } from 'util'; import { memoize } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; -import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { dirname, join, resolve } from 'vs/base/common/path'; @@ -23,7 +22,7 @@ import { findFreePort } from 'vs/base/node/ports'; import { MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes'; import { localize } from 'vs/nls'; import { ISerializableCommandAction } from 'vs/platform/action/common/action'; -import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; +import { INativeOpenDialogOptions, massageMessageBoxOptions } from 'vs/platform/dialogs/common/dialogs'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -288,20 +287,17 @@ export class NativeHostMainService extends Disposable implements INativeHostMain throw error; } - const { response } = await this.showMessageBox(windowId, { - title: this.productService.nameLong, + const { options, buttonIndeces } = massageMessageBoxOptions({ type: 'info', message: localize('warnEscalation', "{0} will now prompt with 'osascript' for Administrator privileges to install the shell command.", this.productService.nameShort), buttons: [ - mnemonicButtonLabel(localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK")), - mnemonicButtonLabel(localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&Cancel")), - ], - noLink: true, - defaultId: 0, - cancelId: 1 - }); + localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK"), + localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&Cancel") + ] + }, this.productService); - if (response === 0 /* OK */) { + const { response } = await this.showMessageBox(windowId, options); + if (buttonIndeces[response] === 0 /* OK */) { try { const command = `osascript -e "do shell script \\"mkdir -p /usr/local/bin && ln -sf \'${target}\' \'${source}\'\\" with administrator privileges"`; await promisify(exec)(command); @@ -320,20 +316,17 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } catch (error) { switch (error.code) { case 'EACCES': { - const { response } = await this.showMessageBox(windowId, { - title: this.productService.nameLong, + const { options, buttonIndeces } = massageMessageBoxOptions({ type: 'info', message: localize('warnEscalationUninstall', "{0} will now prompt with 'osascript' for Administrator privileges to uninstall the shell command.", this.productService.nameShort), buttons: [ - mnemonicButtonLabel(localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK")), - mnemonicButtonLabel(localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&Cancel")), - ], - noLink: true, - defaultId: 0, - cancelId: 1 - }); - - if (response === 0 /* OK */) { + localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK"), + localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&Cancel"), + ] + }, this.productService); + + const { response } = await this.showMessageBox(windowId, options); + if (buttonIndeces[response] === 0 /* OK */) { try { const command = `osascript -e "do shell script \\"rm \'${source}\'\\" with administrator privileges"`; await promisify(exec)(command); diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index de665e04adca6..6c82e811fe935 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -8,7 +8,6 @@ import { DeferredPromise, RunOnceScheduler, timeout } from 'vs/base/common/async import { CancellationToken } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; -import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { Disposable } from 'vs/base/common/lifecycle'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { join } from 'vs/base/common/path'; @@ -51,6 +50,7 @@ import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProp import { hostname, release } from 'os'; import { resolveMachineId } from 'vs/platform/telemetry/electron-main/telemetryUtils'; import { ILoggerMainService } from 'vs/platform/log/electron-main/loggerService'; +import { massageMessageBoxOptions } from 'vs/platform/dialogs/common/dialogs'; export interface IWindowCreationOptions { readonly state: IWindowState; @@ -715,25 +715,24 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // Show Dialog - const result = await this.dialogMainService.showMessageBox({ - title: this.productService.nameLong, + const { options, buttonIndeces } = massageMessageBoxOptions({ type: 'warning', buttons: [ - mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), - mnemonicButtonLabel(localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), - mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close")) + localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen"), + localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"), + localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting") ], message: localize('appStalled', "The window is not responding"), detail: localize('appStalledDetail', "You can reopen or close the window or keep waiting."), - noLink: true, - defaultId: 0, - cancelId: 1, checkboxLabel: this._config?.workspace ? localize('doNotRestoreEditors', "Don't restore editors") : undefined - }, this._win); + }, this.productService); + + const result = await this.dialogMainService.showMessageBox(options, this._win); + const buttonIndex = buttonIndeces[result.response]; // Handle choice - if (result.response !== 1 /* keep waiting */) { - const reopen = result.response === 0; + if (buttonIndex !== 1 /* keep waiting */) { + const reopen = buttonIndex === 0; await this.destroyWindow(reopen, result.checkboxChecked); } } @@ -759,26 +758,23 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // Show Dialog - const result = await this.dialogMainService.showMessageBox({ - title: this.productService.nameLong, + const { options, buttonIndeces } = massageMessageBoxOptions({ type: 'warning', buttons: [ - this._config?.workspace ? - mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")) : - mnemonicButtonLabel(localize({ key: 'newWindow', comment: ['&& denotes a mnemonic'] }, "&&New Window")), - mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close")) + this._config?.workspace ? localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen") : localize({ key: 'newWindow', comment: ['&& denotes a mnemonic'] }, "&&New Window"), + localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close") ], message, detail: this._config?.workspace ? localize('appGoneDetailWorkspace', "We are sorry for the inconvenience. You can reopen the window to continue where you left off.") : localize('appGoneDetailEmptyWindow', "We are sorry for the inconvenience. You can open a new empty window to start again."), - noLink: true, - defaultId: 0, checkboxLabel: this._config?.workspace ? localize('doNotRestoreEditors', "Don't restore editors") : undefined - }, this._win); + }, this.productService); + + const result = await this.dialogMainService.showMessageBox(options, this._win); // Handle choice - const reopen = result.response === 0; + const reopen = buttonIndeces[result.response] === 0; await this.destroyWindow(reopen, result.checkboxChecked); } } @@ -822,20 +818,18 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // Inform user - const result = await this.dialogMainService.showMessageBox({ - title: this.productService.nameLong, + const { options, buttonIndeces } = massageMessageBoxOptions({ type: 'error', buttons: [ - mnemonicButtonLabel(localize({ key: 'learnMore', comment: ['&& denotes a mnemonic'] }, "&&Learn More")), - mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close")) + localize({ key: 'learnMore', comment: ['&& denotes a mnemonic'] }, "&&Learn More"), + localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close") ], message: localize('appGoneAdminMessage', "Running as administrator is not supported in your environment"), - detail: localize('appGoneAdminDetail', "We are sorry for the inconvenience. Please try again without administrator privileges.", this.productService.nameLong), - noLink: true, - defaultId: 0 - }, this._win); + detail: localize('appGoneAdminDetail', "We are sorry for the inconvenience. Please try again without administrator privileges.", this.productService.nameLong) + }, this.productService); - if (result.response === 0) { + const result = await this.dialogMainService.showMessageBox(options, this._win); + if (buttonIndeces[result.response] === 0) { await this.nativeHostMainService.openExternal(undefined, 'https://go.microsoft.com/fwlink/?linkid=2220179'); } diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index c8abb90d59a49..8564e8cf05986 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, BrowserWindow, MessageBoxOptions, WebContents } from 'electron'; +import { app, BrowserWindow, WebContents } from 'electron'; import { Promises } from 'vs/base/node/pfs'; import { hostname, release } from 'os'; import { coalesce, distinct, firstOrDefault } from 'vs/base/common/arrays'; @@ -12,7 +12,7 @@ import { CharCode } from 'vs/base/common/charCode'; import { Emitter, Event } from 'vs/base/common/event'; import { isWindowsDriveLetter, parseLineAndColumnAware, sanitizeFilePath, toSlashes } from 'vs/base/common/extpath'; import { once } from 'vs/base/common/functional'; -import { getPathLabel, mnemonicButtonLabel } from 'vs/base/common/labels'; +import { getPathLabel } from 'vs/base/common/labels'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basename, join, normalize, posix } from 'vs/base/common/path'; @@ -55,6 +55,7 @@ import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataPro import { IPolicyService } from 'vs/platform/policy/common/policy'; import { IUserDataProfilesMainService } from 'vs/platform/userDataProfile/electron-main/userDataProfile'; import { ILoggerMainService } from 'vs/platform/log/electron-main/loggerService'; +import { massageMessageBoxOptions } from 'vs/platform/dialogs/common/dialogs'; //#region Helper Interfaces @@ -793,19 +794,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Path does not exist: show a warning box const uri = this.resourceFromOpenable(pathToOpen); - const options: MessageBoxOptions = { - title: this.productService.nameLong, + this.dialogMainService.showMessageBox(massageMessageBoxOptions({ type: 'info', - buttons: [mnemonicButtonLabel(localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK"))], - defaultId: 0, + buttons: [localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK")], message: uri.scheme === Schemas.file ? localize('pathNotExistTitle', "Path does not exist") : localize('uriInvalidTitle', "URI can not be opened"), detail: uri.scheme === Schemas.file ? localize('pathNotExistDetail', "The path '{0}' does not exist on this computer.", getPathLabel(uri, { os: OS, tildify: this.environmentMainService })) : - localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString(true)), - noLink: true - }; - - this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); + localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString(true)) + }, this.productService).options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); return undefined; })); diff --git a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts index 26b3b2fb1caf7..601b85a3f9192 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, MessageBoxOptions } from 'electron'; +import { BrowserWindow } from 'electron'; import { Emitter, Event } from 'vs/base/common/event'; import { parse } from 'vs/base/common/json'; -import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { dirname, join } from 'vs/base/common/path'; @@ -16,6 +15,7 @@ import { URI } from 'vs/base/common/uri'; import { Promises } from 'vs/base/node/pfs'; import { localize } from 'vs/nls'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; +import { massageMessageBoxOptions } from 'vs/platform/dialogs/common/dialogs'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -279,17 +279,12 @@ export class WorkspacesManagementMainService extends Disposable implements IWork // Prevent overwriting a workspace that is currently opened in another window if (findWindowOnWorkspaceOrFolder(windows, workspacePath)) { - const options: MessageBoxOptions = { - title: this.productService.nameLong, + await this.dialogMainService.showMessageBox(massageMessageBoxOptions({ type: 'info', - buttons: [mnemonicButtonLabel(localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK"))], + buttons: [localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK")], message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(workspacePath)), - detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), - noLink: true, - defaultId: 0 - }; - - await this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); + detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again.") + }, this.productService).options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); return false; }