diff --git a/src/vs/base/common/labels.ts b/src/vs/base/common/labels.ts index fb09d81fa5e09..6945ec835419f 100644 --- a/src/vs/base/common/labels.ts +++ b/src/vs/base/common/labels.ts @@ -417,17 +417,27 @@ export function mnemonicMenuLabel(label: string, forceDisableMnemonics?: boolean * - Windows: Supported via & character (replace && with & and & with && for escaping) * - Linux: Supported via _ character (replace && with _) * - macOS: Unsupported (replace && with empty string) + * When forceDisableMnemonics is set, returns just the label without mnemonics. */ -export function mnemonicButtonLabel(label: string, forceDisableMnemonics?: boolean): string { - if (isMacintosh || forceDisableMnemonics) { - return label.replace(/\(&&\w\)|&&/g, ''); +export function mnemonicButtonLabel(label: string, forceDisableMnemonics: true): string; +export function mnemonicButtonLabel(label: string, forceDisableMnemonics?: false): { readonly withMnemonic: string; readonly withoutMnemonic: string }; +export function mnemonicButtonLabel(label: string, forceDisableMnemonics?: boolean): { readonly withMnemonic: string; readonly withoutMnemonic: string } | string { + const withoutMnemonic = label.replace(/\(&&\w\)|&&/g, ''); + + if (forceDisableMnemonics) { + return withoutMnemonic; + } + if (isMacintosh) { + return { withMnemonic: withoutMnemonic, withoutMnemonic }; } + let withMnemonic: string; if (isWindows) { - return label.replace(/&&|&/g, m => m === '&' ? '&&' : '&'); + withMnemonic = label.replace(/&&|&/g, m => m === '&' ? '&&' : '&'); + } else { + withMnemonic = label.replace(/&&/g, '_'); } - - return label.replace(/&&/g, '_'); + return { withMnemonic, withoutMnemonic }; } export function unmnemonicLabel(label: string): string { diff --git a/src/vs/base/test/common/labels.test.ts b/src/vs/base/test/common/labels.test.ts index ba893354d6ddc..9e3feeb4d3448 100644 --- a/src/vs/base/test/common/labels.test.ts +++ b/src/vs/base/test/common/labels.test.ts @@ -145,17 +145,17 @@ suite('Labels', () => { }); test('mnemonicButtonLabel', () => { - assert.strictEqual(labels.mnemonicButtonLabel('Hello World'), 'Hello World'); - assert.strictEqual(labels.mnemonicButtonLabel(''), ''); + assert.strictEqual(labels.mnemonicButtonLabel('Hello World').withMnemonic, 'Hello World'); + assert.strictEqual(labels.mnemonicButtonLabel('').withMnemonic, ''); if (isWindows) { - assert.strictEqual(labels.mnemonicButtonLabel('Hello & World'), 'Hello && World'); - assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do ¬ Save && Continue'); + assert.strictEqual(labels.mnemonicButtonLabel('Hello & World').withMnemonic, 'Hello && World'); + assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue').withMnemonic, 'Do ¬ Save && Continue'); } else if (isMacintosh) { - assert.strictEqual(labels.mnemonicButtonLabel('Hello & World'), 'Hello & World'); - assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do not Save & Continue'); + assert.strictEqual(labels.mnemonicButtonLabel('Hello & World').withMnemonic, 'Hello & World'); + assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue').withMnemonic, 'Do not Save & Continue'); } else { - assert.strictEqual(labels.mnemonicButtonLabel('Hello & World'), 'Hello & World'); - assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do _not Save & Continue'); + assert.strictEqual(labels.mnemonicButtonLabel('Hello & World').withMnemonic, 'Hello & World'); + assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue').withMnemonic, 'Do _not Save & Continue'); } }); diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index 96259d6807eff..32c6e1c4e74db 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -240,7 +240,7 @@ export interface IOpenDialogOptions { /** * A human-readable string for the open button. */ - readonly openLabel?: string; + readonly openLabel?: { readonly withMnemonic: string; readonly withoutMnemonic: string } | string; /** * Allow to select files, defaults to `true`. @@ -640,7 +640,7 @@ export interface IMassagedMessageBoxOptions { export function massageMessageBoxOptions(options: MessageBoxOptions, productService: IProductService): IMassagedMessageBoxOptions { const massagedOptions = deepClone(options); - let buttons = (massagedOptions.buttons ?? []).map(button => mnemonicButtonLabel(button)); + let buttons = (massagedOptions.buttons ?? []).map(button => mnemonicButtonLabel(button).withMnemonic); let buttonIndeces = (options.buttons || []).map((button, index) => index); let defaultId = 0; // by default the first button is default button diff --git a/src/vs/platform/dialogs/electron-main/dialogMainService.ts b/src/vs/platform/dialogs/electron-main/dialogMainService.ts index 102075083afd9..833a51ce90d66 100644 --- a/src/vs/platform/dialogs/electron-main/dialogMainService.ts +++ b/src/vs/platform/dialogs/electron-main/dialogMainService.ts @@ -71,7 +71,7 @@ export class DialogMainService implements IDialogMainService { pickWorkspace(options: INativeOpenDialogOptions, window?: electron.BrowserWindow): Promise { const title = localize('openWorkspaceTitle', "Open Workspace from File"); - const buttonLabel = mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open")); + const buttonLabel = mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open")).withMnemonic; const filters = WORKSPACE_FILTER; return this.doPick({ ...options, pickFiles: true, title, filters, buttonLabel }, window); diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts index da1f105a4845b..44ecb6d65e5ff 100644 --- a/src/vs/workbench/contrib/files/browser/fileImportExport.ts +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -26,7 +26,6 @@ import { isWeb } from '../../../../base/common/platform.js'; import { getActiveWindow, isDragEvent, triggerDownload } from '../../../../base/browser/dom.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { FileAccess, Schemas } from '../../../../base/common/network.js'; -import { mnemonicButtonLabel } from '../../../../base/common/labels.js'; import { listenStream } from '../../../../base/common/stream.js'; import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { createSingleCallFunction } from '../../../../base/common/functional.js'; @@ -828,7 +827,7 @@ export class FileDownload { const destination = await this.fileDialogService.showSaveDialog({ availableFileSystems: [Schemas.file], - saveLabel: mnemonicButtonLabel(localize('downloadButton', "Download")), + saveLabel: localize('downloadButton', "Download"), title: localize('chooseWhereToDownload', "Choose Where to Download"), defaultUri }); diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index af5292d79826d..ffea9e2c6aca9 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -283,7 +283,7 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { this.filePickBox.sortByLabel = false; this.filePickBox.ignoreFocusOut = true; this.filePickBox.ok = true; - this.filePickBox.okLabel = this.options.openLabel; + this.filePickBox.okLabel = typeof this.options.openLabel === 'string' ? this.options.openLabel : this.options.openLabel?.withoutMnemonic; if ((this.scheme !== Schemas.file) && this.options && this.options.availableFileSystems && (this.options.availableFileSystems.length > 1) && (this.options.availableFileSystems.indexOf(Schemas.file) > -1)) { this.filePickBox.customButton = true; this.filePickBox.customLabel = nls.localize('remoteFileDialog.local', 'Show Local'); diff --git a/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts b/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts index 39511840b2147..6b7b2e976018b 100644 --- a/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts @@ -178,7 +178,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil const newOptions: OpenDialogOptions & { properties: string[] } & INativeHostOptions = { title: options.title, defaultPath: options.defaultUri?.fsPath, - buttonLabel: options.openLabel, + buttonLabel: typeof options.openLabel === 'string' ? options.openLabel : options.openLabel?.withMnemonic, filters: options.filters, properties: [], targetWindowId: getActiveWindow().vscodeWindowId diff --git a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts index 25fd78863cbcd..be885f55a7d02 100644 --- a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts @@ -19,7 +19,6 @@ import { INotificationService, Severity } from '../../../../platform/notificatio import { IFileService } from '../../../../platform/files/common/files.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; import { IFileDialogService, IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { mnemonicButtonLabel } from '../../../../base/common/labels.js'; import { ITextFileService } from '../../textfile/common/textfiles.js'; import { IHostService } from '../../host/browser/host.js'; import { Schemas } from '../../../../base/common/network.js'; @@ -62,7 +61,7 @@ export abstract class AbstractWorkspaceEditingService extends Disposable impleme availableFileSystems.unshift(Schemas.vscodeRemote); } let workspacePath = await this.fileDialogService.showSaveDialog({ - saveLabel: mnemonicButtonLabel(localize('save', "Save")), + saveLabel: localize('save', "Save"), title: localize('saveWorkspace', "Save Workspace"), filters: WORKSPACE_FILTER, defaultUri: joinPath(await this.fileDialogService.defaultWorkspacePath(), this.getNewWorkspaceName()),