Path: blob/main/src/vs/platform/dialogs/common/dialogs.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Event } from '../../../base/common/event.js';6import { ThemeIcon } from '../../../base/common/themables.js';7import { IMarkdownString } from '../../../base/common/htmlContent.js';8import { basename } from '../../../base/common/resources.js';9import Severity from '../../../base/common/severity.js';10import { URI } from '../../../base/common/uri.js';11import { localize } from '../../../nls.js';12import { createDecorator } from '../../instantiation/common/instantiation.js';13import { ITelemetryData } from '../../telemetry/common/telemetry.js';14import { MessageBoxOptions } from '../../../base/parts/sandbox/common/electronTypes.js';15import { mnemonicButtonLabel } from '../../../base/common/labels.js';16import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js';17import { IProductService } from '../../product/common/productService.js';18import { deepClone } from '../../../base/common/objects.js';1920export interface IDialogArgs {21readonly confirmArgs?: IConfirmDialogArgs;22readonly inputArgs?: IInputDialogArgs;23readonly promptArgs?: IPromptDialogArgs;24}2526export interface IBaseDialogOptions {27readonly type?: Severity | DialogType;2829readonly title?: string;30readonly message: string;31readonly detail?: string;3233readonly checkbox?: ICheckbox;3435/**36* Allows to enforce use of custom dialog even in native environments.37*/38readonly custom?: boolean | ICustomDialogOptions;39}4041export interface IConfirmDialogArgs {42readonly confirmation: IConfirmation;43}4445export interface IConfirmation extends IBaseDialogOptions {4647/**48* If not provided, defaults to `Yes`.49*/50readonly primaryButton?: string;5152/**53* If not provided, defaults to `Cancel`.54*/55readonly cancelButton?: string;56}5758export interface IConfirmationResult extends ICheckboxResult {5960/**61* Will be true if the dialog was confirmed with the primary button pressed.62*/63readonly confirmed: boolean;64}6566export interface IInputDialogArgs {67readonly input: IInput;68}6970export interface IInput extends IConfirmation {71readonly inputs: IInputElement[];7273/**74* If not provided, defaults to `Ok`.75*/76readonly primaryButton?: string;77}7879export interface IInputElement {80readonly type?: 'text' | 'password';81readonly value?: string;82readonly placeholder?: string;83}8485export interface IInputResult extends IConfirmationResult {8687/**88* Values for the input fields as provided by the user or `undefined` if none.89*/90readonly values?: string[];91}9293export interface IPromptDialogArgs {94readonly prompt: IPrompt<unknown>;95}9697export interface IPromptBaseButton<T> {9899/**100* @returns the result of the prompt button will be returned101* as result from the `prompt()` call.102*/103run(checkbox: ICheckboxResult): T | Promise<T>;104}105106export interface IPromptButton<T> extends IPromptBaseButton<T> {107readonly label: string;108}109110export interface IPromptCancelButton<T> extends IPromptBaseButton<T> {111112/**113* The cancel button to show in the prompt. Defaults to114* `Cancel` if not provided.115*/116readonly label?: string;117}118119export interface IPrompt<T> extends IBaseDialogOptions {120121/**122* The buttons to show in the prompt. Defaults to `OK`123* if no buttons or cancel button is provided.124*/125readonly buttons?: IPromptButton<T>[];126127/**128* The cancel button to show in the prompt. Defaults to129* `Cancel` if set to `true`.130*/131readonly cancelButton?: IPromptCancelButton<T> | true | string;132}133134export interface IPromptWithCustomCancel<T> extends IPrompt<T> {135readonly cancelButton: IPromptCancelButton<T>;136}137138export interface IPromptWithDefaultCancel<T> extends IPrompt<T> {139readonly cancelButton: true | string;140}141142export interface IPromptResult<T> extends ICheckboxResult {143144/**145* The result of the `IPromptButton` that was pressed or `undefined` if none.146*/147readonly result?: T;148}149150export interface IPromptResultWithCancel<T> extends IPromptResult<T> {151readonly result: T;152}153154export interface IAsyncPromptResult<T> extends ICheckboxResult {155156/**157* The result of the `IPromptButton` that was pressed or `undefined` if none.158*/159readonly result?: Promise<T>;160}161162export interface IAsyncPromptResultWithCancel<T> extends IAsyncPromptResult<T> {163readonly result: Promise<T>;164}165166export type IDialogResult = IConfirmationResult | IInputResult | IAsyncPromptResult<unknown>;167168export type DialogType = 'none' | 'info' | 'error' | 'question' | 'warning';169170export interface ICheckbox {171readonly label: string;172readonly checked?: boolean;173}174175export interface ICheckboxResult {176177/**178* This will only be defined if the confirmation was created179* with the checkbox option defined.180*/181readonly checkboxChecked?: boolean;182}183184export interface IPickAndOpenOptions {185readonly forceNewWindow?: boolean;186defaultUri?: URI;187readonly telemetryExtraData?: ITelemetryData;188availableFileSystems?: string[];189remoteAuthority?: string | null;190}191192export interface FileFilter {193readonly extensions: string[];194readonly name: string;195}196197export interface ISaveDialogOptions {198199/**200* A human-readable string for the dialog title201*/202title?: string;203204/**205* The resource the dialog shows when opened.206*/207defaultUri?: URI;208209/**210* A set of file filters that are used by the dialog. Each entry is a human readable label,211* like "TypeScript", and an array of extensions.212*/213filters?: FileFilter[];214215/**216* A human-readable string for the ok button217*/218readonly saveLabel?: { readonly withMnemonic: string; readonly withoutMnemonic: string } | string;219220/**221* Specifies a list of schemas for the file systems the user can save to. If not specified, uses the schema of the defaultURI or, if also not specified,222* the schema of the current window.223*/224availableFileSystems?: readonly string[];225}226227export interface IOpenDialogOptions {228229/**230* A human-readable string for the dialog title231*/232readonly title?: string;233234/**235* The resource the dialog shows when opened.236*/237defaultUri?: URI;238239/**240* A human-readable string for the open button.241*/242readonly openLabel?: { readonly withMnemonic: string; readonly withoutMnemonic: string } | string;243244/**245* Allow to select files, defaults to `true`.246*/247canSelectFiles?: boolean;248249/**250* Allow to select folders, defaults to `false`.251*/252canSelectFolders?: boolean;253254/**255* Allow to select many files or folders.256*/257readonly canSelectMany?: boolean;258259/**260* A set of file filters that are used by the dialog. Each entry is a human readable label,261* like "TypeScript", and an array of extensions.262*/263filters?: FileFilter[];264265/**266* Specifies a list of schemas for the file systems the user can load from. If not specified, uses the schema of the defaultURI or, if also not available,267* the schema of the current window.268*/269availableFileSystems?: readonly string[];270}271272export const IDialogService = createDecorator<IDialogService>('dialogService');273274export interface ICustomDialogOptions {275readonly buttonDetails?: string[];276readonly markdownDetails?: ICustomDialogMarkdown[];277readonly classes?: string[];278readonly icon?: ThemeIcon;279readonly disableCloseAction?: boolean;280}281282export interface ICustomDialogMarkdown {283readonly markdown: IMarkdownString;284readonly classes?: string[];285/** Custom link handler for markdown content, see {@link IContentActionHandler}. Defaults to {@link openLinkFromMarkdown}. */286actionHandler?(link: string): Promise<boolean>;287}288289/**290* A handler to bring up modal dialogs.291*/292export interface IDialogHandler {293294/**295* Ask the user for confirmation with a modal dialog.296*/297confirm(confirmation: IConfirmation): Promise<IConfirmationResult>;298299/**300* Prompt the user with a modal dialog.301*/302prompt<T>(prompt: IPrompt<T>): Promise<IAsyncPromptResult<T>>;303304/**305* Present a modal dialog to the user asking for input.306*/307input(input: IInput): Promise<IInputResult>;308309/**310* Present the about dialog to the user.311*/312about(title: string, details: string, detailsToCopy: string): Promise<void>;313}314315enum DialogKind {316Confirmation = 1,317Prompt,318Input319}320321export abstract class AbstractDialogHandler implements IDialogHandler {322323protected getConfirmationButtons(dialog: IConfirmation): string[] {324return this.getButtons(dialog, DialogKind.Confirmation);325}326327protected getPromptButtons(dialog: IPrompt<unknown>): string[] {328return this.getButtons(dialog, DialogKind.Prompt);329}330331protected getInputButtons(dialog: IInput): string[] {332return this.getButtons(dialog, DialogKind.Input);333}334335private getButtons(dialog: IConfirmation, kind: DialogKind.Confirmation): string[];336private getButtons(dialog: IPrompt<unknown>, kind: DialogKind.Prompt): string[];337private getButtons(dialog: IInput, kind: DialogKind.Input): string[];338private getButtons(dialog: IConfirmation | IInput | IPrompt<unknown>, kind: DialogKind): string[] {339340// We put buttons in the order of "default" button first and "cancel"341// button last. There maybe later processing when presenting the buttons342// based on OS standards.343344const buttons: string[] = [];345346switch (kind) {347case DialogKind.Confirmation: {348const confirmationDialog = dialog as IConfirmation;349350if (confirmationDialog.primaryButton) {351buttons.push(confirmationDialog.primaryButton);352} else {353buttons.push(localize({ key: 'yesButton', comment: ['&& denotes a mnemonic'] }, "&&Yes"));354}355356if (confirmationDialog.cancelButton) {357buttons.push(confirmationDialog.cancelButton);358} else {359buttons.push(localize('cancelButton', "Cancel"));360}361362break;363}364case DialogKind.Prompt: {365const promptDialog = dialog as IPrompt<unknown>;366367if (Array.isArray(promptDialog.buttons) && promptDialog.buttons.length > 0) {368buttons.push(...promptDialog.buttons.map(button => button.label));369}370371if (promptDialog.cancelButton) {372if (promptDialog.cancelButton === true) {373buttons.push(localize('cancelButton', "Cancel"));374} else if (typeof promptDialog.cancelButton === 'string') {375buttons.push(promptDialog.cancelButton);376} else {377if (promptDialog.cancelButton.label) {378buttons.push(promptDialog.cancelButton.label);379} else {380buttons.push(localize('cancelButton', "Cancel"));381}382}383}384385if (buttons.length === 0) {386buttons.push(localize({ key: 'okButton', comment: ['&& denotes a mnemonic'] }, "&&OK"));387}388389break;390}391case DialogKind.Input: {392const inputDialog = dialog as IInput;393394if (inputDialog.primaryButton) {395buttons.push(inputDialog.primaryButton);396} else {397buttons.push(localize({ key: 'okButton', comment: ['&& denotes a mnemonic'] }, "&&OK"));398}399400if (inputDialog.cancelButton) {401buttons.push(inputDialog.cancelButton);402} else {403buttons.push(localize('cancelButton', "Cancel"));404}405406break;407}408}409410return buttons;411}412413protected getDialogType(type: Severity | DialogType | undefined): DialogType | undefined {414if (typeof type === 'string') {415return type;416}417418if (typeof type === 'number') {419return (type === Severity.Info) ? 'info' : (type === Severity.Error) ? 'error' : (type === Severity.Warning) ? 'warning' : 'none';420}421422return undefined;423}424425protected getPromptResult<T>(prompt: IPrompt<T>, buttonIndex: number, checkboxChecked: boolean | undefined): IAsyncPromptResult<T> {426const promptButtons: IPromptBaseButton<T>[] = [...(prompt.buttons ?? [])];427if (prompt.cancelButton && typeof prompt.cancelButton !== 'string' && typeof prompt.cancelButton !== 'boolean') {428promptButtons.push(prompt.cancelButton);429}430431let result = promptButtons[buttonIndex]?.run({ checkboxChecked });432if (!(result instanceof Promise)) {433result = Promise.resolve(result);434}435436return { result, checkboxChecked };437}438439abstract confirm(confirmation: IConfirmation): Promise<IConfirmationResult>;440abstract input(input: IInput): Promise<IInputResult>;441abstract prompt<T>(prompt: IPrompt<T>): Promise<IAsyncPromptResult<T>>;442abstract about(title: string, details: string, detailsToCopy: string): Promise<void>;443}444445/**446* A service to bring up modal dialogs.447*448* Note: use the `INotificationService.prompt()` method for a non-modal way to ask449* the user for input.450*/451export interface IDialogService {452453readonly _serviceBrand: undefined;454455/**456* An event that fires when a dialog is about to show.457*/458onWillShowDialog: Event<void>;459460/**461* An event that fires when a dialog did show (closed).462*/463onDidShowDialog: Event<void>;464465/**466* Ask the user for confirmation with a modal dialog.467*/468confirm(confirmation: IConfirmation): Promise<IConfirmationResult>;469470/**471* Prompt the user with a modal dialog. Provides a bit472* more control over the dialog compared to the simpler473* `confirm` method. Specifically, allows to show more474* than 2 buttons and makes it easier to just show a475* message to the user.476*477* @returns a promise that resolves to the `T` result478* from the provided `IPromptButton<T>` or `undefined`.479*/480prompt<T>(prompt: IPromptWithCustomCancel<T>): Promise<IPromptResultWithCancel<T>>;481prompt<T>(prompt: IPromptWithDefaultCancel<T>): Promise<IPromptResult<T>>;482prompt<T>(prompt: IPrompt<T>): Promise<IPromptResult<T>>;483484/**485* Present a modal dialog to the user asking for input.486*/487input(input: IInput): Promise<IInputResult>;488489/**490* Show a modal info dialog.491*/492info(message: string, detail?: string): Promise<void>;493494/**495* Show a modal warning dialog.496*/497warn(message: string, detail?: string): Promise<void>;498499/**500* Show a modal error dialog.501*/502error(message: string, detail?: string): Promise<void>;503504/**505* Present the about dialog to the user.506*/507about(): Promise<void>;508}509510export const IFileDialogService = createDecorator<IFileDialogService>('fileDialogService');511512/**513* A service to bring up file dialogs.514*/515export interface IFileDialogService {516517readonly _serviceBrand: undefined;518519/**520* The default path for a new file based on previously used files.521* @param schemeFilter The scheme of the file path. If no filter given, the scheme of the current window is used.522* Falls back to user home in the absence of enough information to find a better URI.523*/524defaultFilePath(schemeFilter?: string): Promise<URI>;525526/**527* The default path for a new folder based on previously used folders.528* @param schemeFilter The scheme of the folder path. If no filter given, the scheme of the current window is used.529* Falls back to user home in the absence of enough information to find a better URI.530*/531defaultFolderPath(schemeFilter?: string): Promise<URI>;532533/**534* The default path for a new workspace based on previously used workspaces.535* @param schemeFilter The scheme of the workspace path. If no filter given, the scheme of the current window is used.536* Falls back to user home in the absence of enough information to find a better URI.537*/538defaultWorkspacePath(schemeFilter?: string): Promise<URI>;539540/**541* Shows a file-folder selection dialog and opens the selected entry.542*/543pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;544545/**546* Shows a file selection dialog and opens the selected entry.547*/548pickFileAndOpen(options: IPickAndOpenOptions): Promise<void>;549550/**551* Shows a folder selection dialog and opens the selected entry.552*/553pickFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;554555/**556* Shows a workspace selection dialog and opens the selected entry.557*/558pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void>;559560/**561* Shows a save file dialog and save the file at the chosen file URI.562*/563pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined>;564565/**566* The preferred folder path to open the dialog at.567* @param schemeFilter The scheme of the file path. If no filter given, the scheme of the current window is used.568* Falls back to user home in the absence of a setting.569*/570preferredHome(schemeFilter?: string): Promise<URI>;571572/**573* Shows a save file dialog and returns the chosen file URI.574*/575showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined>;576577/**578* Shows a confirm dialog for saving 1-N files.579*/580showSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise<ConfirmResult>;581582/**583* Shows a open file dialog and returns the chosen file URI.584*/585showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined>;586}587588export const enum ConfirmResult {589SAVE,590DONT_SAVE,591CANCEL592}593594const MAX_CONFIRM_FILES = 10;595export function getFileNamesMessage(fileNamesOrResources: readonly (string | URI)[]): string {596const message: string[] = [];597message.push(...fileNamesOrResources.slice(0, MAX_CONFIRM_FILES).map(fileNameOrResource => typeof fileNameOrResource === 'string' ? fileNameOrResource : basename(fileNameOrResource)));598599if (fileNamesOrResources.length > MAX_CONFIRM_FILES) {600if (fileNamesOrResources.length - MAX_CONFIRM_FILES === 1) {601message.push(localize('moreFile', "...1 additional file not shown"));602} else {603message.push(localize('moreFiles', "...{0} additional files not shown", fileNamesOrResources.length - MAX_CONFIRM_FILES));604}605}606607message.push('');608return message.join('\n');609}610611export interface INativeOpenDialogOptions {612readonly forceNewWindow?: boolean;613614readonly defaultPath?: string;615616readonly telemetryEventName?: string;617readonly telemetryExtraData?: ITelemetryData;618}619620export interface IMassagedMessageBoxOptions {621622/**623* OS massaged message box options.624*/625readonly options: MessageBoxOptions;626627/**628* Since the massaged result of the message box options potentially629* changes the order of buttons, we have to keep a map of these630* changes so that we can still return the correct index to the caller.631*/632readonly buttonIndeces: number[];633}634635/**636* A utility method to ensure the options for the message box dialog637* are using properties that are consistent across all platforms and638* specific to the platform where necessary.639*/640export function massageMessageBoxOptions(options: MessageBoxOptions, productService: IProductService): IMassagedMessageBoxOptions {641const massagedOptions = deepClone(options);642643let buttons = (massagedOptions.buttons ?? []).map(button => mnemonicButtonLabel(button).withMnemonic);644let buttonIndeces = (options.buttons || []).map((button, index) => index);645646let defaultId = 0; // by default the first button is default button647let cancelId = massagedOptions.cancelId ?? buttons.length - 1; // by default the last button is cancel button648649// Apply HIG per OS when more than one button is used650if (buttons.length > 1) {651const cancelButton = typeof cancelId === 'number' ? buttons[cancelId] : undefined;652653if (isLinux || isMacintosh) {654655// Linux: the GNOME HIG (https://developer.gnome.org/hig/patterns/feedback/dialogs.html?highlight=dialog)656// recommend the following:657// "Always ensure that the cancel button appears first, before the affirmative button. In left-to-right658// locales, this is on the left. This button order ensures that users become aware of, and are reminded659// of, the ability to cancel prior to encountering the affirmative button."660//661// Electron APIs do not reorder buttons for us, so we ensure a reverse order of buttons and a position662// of the cancel button (if provided) that matches the HIG663664// macOS: the HIG (https://developer.apple.com/design/human-interface-guidelines/components/presentation/alerts)665// recommend the following:666// "Place buttons where people expect. In general, place the button people are most likely to choose on the trailing side in a667// 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 the668// top of a stack. Cancel buttons are typically on the leading side of a row or at the bottom of a stack."669//670// However: it seems that older macOS versions where 3 buttons were presented in a row differ from this671// recommendation. In fact, cancel buttons were placed to the left of the default button and secondary672// buttons on the far left. To support these older macOS versions we have to manually shuffle the cancel673// button in the same way as we do on Linux. This will not have any impact on newer macOS versions where674// shuffling is done for us.675676if (typeof cancelButton === 'string' && buttons.length > 1 && cancelId !== 1) {677buttons.splice(cancelId, 1);678buttons.splice(1, 0, cancelButton);679680const cancelButtonIndex = buttonIndeces[cancelId];681buttonIndeces.splice(cancelId, 1);682buttonIndeces.splice(1, 0, cancelButtonIndex);683684cancelId = 1;685}686687if (isLinux && buttons.length > 1) {688buttons = buttons.reverse();689buttonIndeces = buttonIndeces.reverse();690691defaultId = buttons.length - 1;692if (typeof cancelButton === 'string') {693cancelId = defaultId - 1;694}695}696} else if (isWindows) {697698// Windows: the HIG (https://learn.microsoft.com/en-us/windows/win32/uxguide/win-dialog-box)699// recommend the following:700// "One of the following sets of concise commands: Yes/No, Yes/No/Cancel, [Do it]/Cancel,701// [Do it]/[Don't do it], [Do it]/[Don't do it]/Cancel."702//703// Electron APIs do not reorder buttons for us, so we ensure the position of the cancel button704// (if provided) that matches the HIG705706if (typeof cancelButton === 'string' && buttons.length > 1 && cancelId !== buttons.length - 1 /* last action */) {707buttons.splice(cancelId, 1);708buttons.push(cancelButton);709710const buttonIndex = buttonIndeces[cancelId];711buttonIndeces.splice(cancelId, 1);712buttonIndeces.push(buttonIndex);713714cancelId = buttons.length - 1;715}716}717}718719massagedOptions.buttons = buttons;720massagedOptions.defaultId = defaultId;721massagedOptions.cancelId = cancelId;722massagedOptions.noLink = true;723massagedOptions.title = massagedOptions.title || productService.nameLong;724725return {726options: massagedOptions,727buttonIndeces728};729}730731732