mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 08:15:56 +01:00
743 lines
22 KiB
TypeScript
743 lines
22 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { CancellationToken } from '../../../base/common/cancellation.js';
|
|
import { Event } from '../../../base/common/event.js';
|
|
import { ThemeIcon } from '../../../base/common/themables.js';
|
|
import { IMarkdownString } from '../../../base/common/htmlContent.js';
|
|
import { basename } from '../../../base/common/resources.js';
|
|
import Severity from '../../../base/common/severity.js';
|
|
import { URI } from '../../../base/common/uri.js';
|
|
import { localize } from '../../../nls.js';
|
|
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
|
import { ITelemetryData } from '../../telemetry/common/telemetry.js';
|
|
import { MessageBoxOptions } from '../../../base/parts/sandbox/common/electronTypes.js';
|
|
import { mnemonicButtonLabel } from '../../../base/common/labels.js';
|
|
import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js';
|
|
import { IProductService } from '../../product/common/productService.js';
|
|
import { deepClone } from '../../../base/common/objects.js';
|
|
|
|
export interface IDialogArgs {
|
|
readonly confirmArgs?: IConfirmDialogArgs;
|
|
readonly inputArgs?: IInputDialogArgs;
|
|
readonly promptArgs?: IPromptDialogArgs;
|
|
}
|
|
|
|
export interface IBaseDialogOptions {
|
|
readonly type?: Severity | DialogType;
|
|
|
|
readonly title?: string;
|
|
readonly message: string;
|
|
readonly detail?: string;
|
|
|
|
readonly checkbox?: ICheckbox;
|
|
|
|
/**
|
|
* Allows to enforce use of custom dialog even in native environments.
|
|
*/
|
|
readonly custom?: boolean | ICustomDialogOptions;
|
|
|
|
/**
|
|
* An optional cancellation token that can be used to dismiss the dialog
|
|
* programmatically for custom dialog implementations.
|
|
*
|
|
* When cancelled, the custom dialog resolves as if the cancel button was
|
|
* pressed. Native dialog handlers cannot currently be dismissed
|
|
* programmatically and ignore this option unless a custom dialog is
|
|
* explicitly enforced via the {@link custom} option.
|
|
*/
|
|
readonly token?: CancellationToken;
|
|
}
|
|
|
|
export interface IConfirmDialogArgs {
|
|
readonly confirmation: IConfirmation;
|
|
}
|
|
|
|
export interface IConfirmation extends IBaseDialogOptions {
|
|
|
|
/**
|
|
* If not provided, defaults to `Yes`.
|
|
*/
|
|
readonly primaryButton?: string;
|
|
|
|
/**
|
|
* If not provided, defaults to `Cancel`.
|
|
*/
|
|
readonly cancelButton?: string;
|
|
}
|
|
|
|
export interface IConfirmationResult extends ICheckboxResult {
|
|
|
|
/**
|
|
* Will be true if the dialog was confirmed with the primary button pressed.
|
|
*/
|
|
readonly confirmed: boolean;
|
|
}
|
|
|
|
export interface IInputDialogArgs {
|
|
readonly input: IInput;
|
|
}
|
|
|
|
export interface IInput extends IConfirmation {
|
|
readonly inputs: IInputElement[];
|
|
|
|
/**
|
|
* If not provided, defaults to `Ok`.
|
|
*/
|
|
readonly primaryButton?: string;
|
|
}
|
|
|
|
export interface IInputElement {
|
|
readonly type?: 'text' | 'password';
|
|
readonly value?: string;
|
|
readonly placeholder?: string;
|
|
}
|
|
|
|
export interface IInputResult extends IConfirmationResult {
|
|
|
|
/**
|
|
* Values for the input fields as provided by the user or `undefined` if none.
|
|
*/
|
|
readonly values?: string[];
|
|
}
|
|
|
|
export interface IPromptDialogArgs {
|
|
readonly prompt: IPrompt<unknown>;
|
|
}
|
|
|
|
export interface IPromptBaseButton<T> {
|
|
|
|
/**
|
|
* @returns the result of the prompt button will be returned
|
|
* as result from the `prompt()` call.
|
|
*/
|
|
run(checkbox: ICheckboxResult): T | Promise<T>;
|
|
}
|
|
|
|
export interface IPromptButton<T> extends IPromptBaseButton<T> {
|
|
readonly label: string;
|
|
}
|
|
|
|
export interface IPromptCancelButton<T> extends IPromptBaseButton<T> {
|
|
|
|
/**
|
|
* The cancel button to show in the prompt. Defaults to
|
|
* `Cancel` if not provided.
|
|
*/
|
|
readonly label?: string;
|
|
}
|
|
|
|
export interface IPrompt<T> extends IBaseDialogOptions {
|
|
|
|
/**
|
|
* The buttons to show in the prompt. Defaults to `OK`
|
|
* if no buttons or cancel button is provided.
|
|
*/
|
|
readonly buttons?: IPromptButton<T>[];
|
|
|
|
/**
|
|
* The cancel button to show in the prompt. Defaults to
|
|
* `Cancel` if set to `true`.
|
|
*/
|
|
readonly cancelButton?: IPromptCancelButton<T> | true | string;
|
|
}
|
|
|
|
export interface IPromptWithCustomCancel<T> extends IPrompt<T> {
|
|
readonly cancelButton: IPromptCancelButton<T>;
|
|
}
|
|
|
|
export interface IPromptWithDefaultCancel<T> extends IPrompt<T> {
|
|
readonly cancelButton: true | string;
|
|
}
|
|
|
|
export interface IPromptResult<T> extends ICheckboxResult {
|
|
|
|
/**
|
|
* The result of the `IPromptButton` that was pressed or `undefined` if none.
|
|
*/
|
|
readonly result?: T;
|
|
}
|
|
|
|
export interface IPromptResultWithCancel<T> extends IPromptResult<T> {
|
|
readonly result: T;
|
|
}
|
|
|
|
export interface IAsyncPromptResult<T> extends ICheckboxResult {
|
|
|
|
/**
|
|
* The result of the `IPromptButton` that was pressed or `undefined` if none.
|
|
*/
|
|
readonly result?: Promise<T>;
|
|
}
|
|
|
|
export interface IAsyncPromptResultWithCancel<T> extends IAsyncPromptResult<T> {
|
|
readonly result: Promise<T>;
|
|
}
|
|
|
|
export type IDialogResult = IConfirmationResult | IInputResult | IAsyncPromptResult<unknown>;
|
|
|
|
export type DialogType = 'none' | 'info' | 'error' | 'question' | 'warning';
|
|
|
|
export interface ICheckbox {
|
|
readonly label: string;
|
|
readonly checked?: boolean;
|
|
}
|
|
|
|
export interface ICheckboxResult {
|
|
|
|
/**
|
|
* This will only be defined if the confirmation was created
|
|
* with the checkbox option defined.
|
|
*/
|
|
readonly checkboxChecked?: boolean;
|
|
}
|
|
|
|
export interface IPickAndOpenOptions {
|
|
readonly forceNewWindow?: boolean;
|
|
defaultUri?: URI;
|
|
readonly telemetryExtraData?: ITelemetryData;
|
|
availableFileSystems?: string[];
|
|
remoteAuthority?: string | null;
|
|
}
|
|
|
|
export interface FileFilter {
|
|
readonly extensions: string[];
|
|
readonly name: string;
|
|
}
|
|
|
|
export interface ISaveDialogOptions {
|
|
|
|
/**
|
|
* A human-readable string for the dialog title
|
|
*/
|
|
title?: string;
|
|
|
|
/**
|
|
* The resource the dialog shows when opened.
|
|
*/
|
|
defaultUri?: URI;
|
|
|
|
/**
|
|
* A set of file filters that are used by the dialog. Each entry is a human readable label,
|
|
* like "TypeScript", and an array of extensions.
|
|
*/
|
|
filters?: FileFilter[];
|
|
|
|
/**
|
|
* A human-readable string for the ok button
|
|
*/
|
|
readonly saveLabel?: { readonly withMnemonic: string; readonly withoutMnemonic: string } | string;
|
|
|
|
/**
|
|
* 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,
|
|
* the schema of the current window.
|
|
*/
|
|
availableFileSystems?: readonly string[];
|
|
}
|
|
|
|
export interface IOpenDialogOptions {
|
|
|
|
/**
|
|
* A human-readable string for the dialog title
|
|
*/
|
|
readonly title?: string;
|
|
|
|
/**
|
|
* The resource the dialog shows when opened.
|
|
*/
|
|
defaultUri?: URI;
|
|
|
|
/**
|
|
* A human-readable string for the open button.
|
|
*/
|
|
readonly openLabel?: { readonly withMnemonic: string; readonly withoutMnemonic: string } | string;
|
|
|
|
/**
|
|
* Allow to select files, defaults to `true`.
|
|
*/
|
|
canSelectFiles?: boolean;
|
|
|
|
/**
|
|
* Allow to select folders, defaults to `false`.
|
|
*/
|
|
canSelectFolders?: boolean;
|
|
|
|
/**
|
|
* Allow to select many files or folders.
|
|
*/
|
|
readonly canSelectMany?: boolean;
|
|
|
|
/**
|
|
* A set of file filters that are used by the dialog. Each entry is a human readable label,
|
|
* like "TypeScript", and an array of extensions.
|
|
*/
|
|
filters?: FileFilter[];
|
|
|
|
/**
|
|
* 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,
|
|
* the schema of the current window.
|
|
*/
|
|
availableFileSystems?: readonly string[];
|
|
}
|
|
|
|
export const IDialogService = createDecorator<IDialogService>('dialogService');
|
|
|
|
export interface ICustomDialogOptions {
|
|
readonly buttonDetails?: string[];
|
|
readonly markdownDetails?: ICustomDialogMarkdown[];
|
|
readonly classes?: string[];
|
|
readonly icon?: ThemeIcon;
|
|
readonly disableCloseAction?: boolean;
|
|
}
|
|
|
|
export interface ICustomDialogMarkdown {
|
|
readonly markdown: IMarkdownString;
|
|
readonly classes?: string[];
|
|
/** Custom link handler for markdown content, see {@link IContentActionHandler}. Defaults to {@link openLinkFromMarkdown}. */
|
|
actionHandler?(link: string): Promise<boolean>;
|
|
}
|
|
|
|
/**
|
|
* A handler to bring up modal dialogs.
|
|
*/
|
|
export interface IDialogHandler {
|
|
|
|
/**
|
|
* Ask the user for confirmation with a modal dialog.
|
|
*/
|
|
confirm(confirmation: IConfirmation): Promise<IConfirmationResult>;
|
|
|
|
/**
|
|
* Prompt the user with a modal dialog.
|
|
*/
|
|
prompt<T>(prompt: IPrompt<T>): Promise<IAsyncPromptResult<T>>;
|
|
|
|
/**
|
|
* Present a modal dialog to the user asking for input.
|
|
*/
|
|
input(input: IInput): Promise<IInputResult>;
|
|
|
|
/**
|
|
* Present the about dialog to the user.
|
|
*/
|
|
about(title: string, details: string, detailsToCopy: string): Promise<void>;
|
|
}
|
|
|
|
enum DialogKind {
|
|
Confirmation = 1,
|
|
Prompt,
|
|
Input
|
|
}
|
|
|
|
export abstract class AbstractDialogHandler implements IDialogHandler {
|
|
|
|
protected getConfirmationButtons(dialog: IConfirmation): string[] {
|
|
return this.getButtons(dialog, DialogKind.Confirmation);
|
|
}
|
|
|
|
protected getPromptButtons(dialog: IPrompt<unknown>): string[] {
|
|
return this.getButtons(dialog, DialogKind.Prompt);
|
|
}
|
|
|
|
protected getInputButtons(dialog: IInput): string[] {
|
|
return this.getButtons(dialog, DialogKind.Input);
|
|
}
|
|
|
|
private getButtons(dialog: IConfirmation, kind: DialogKind.Confirmation): string[];
|
|
private getButtons(dialog: IPrompt<unknown>, kind: DialogKind.Prompt): string[];
|
|
private getButtons(dialog: IInput, kind: DialogKind.Input): string[];
|
|
private getButtons(dialog: IConfirmation | IInput | IPrompt<unknown>, kind: DialogKind): string[] {
|
|
|
|
// We put buttons in the order of "default" button first and "cancel"
|
|
// button last. There maybe later processing when presenting the buttons
|
|
// based on OS standards.
|
|
|
|
const buttons: string[] = [];
|
|
|
|
switch (kind) {
|
|
case DialogKind.Confirmation: {
|
|
const confirmationDialog = dialog as IConfirmation;
|
|
|
|
if (confirmationDialog.primaryButton) {
|
|
buttons.push(confirmationDialog.primaryButton);
|
|
} else {
|
|
buttons.push(localize({ key: 'yesButton', comment: ['&& denotes a mnemonic'] }, "&&Yes"));
|
|
}
|
|
|
|
if (confirmationDialog.cancelButton) {
|
|
buttons.push(confirmationDialog.cancelButton);
|
|
} else {
|
|
buttons.push(localize('cancelButton', "Cancel"));
|
|
}
|
|
|
|
break;
|
|
}
|
|
case DialogKind.Prompt: {
|
|
const promptDialog = dialog as IPrompt<unknown>;
|
|
|
|
if (Array.isArray(promptDialog.buttons) && promptDialog.buttons.length > 0) {
|
|
buttons.push(...promptDialog.buttons.map(button => button.label));
|
|
}
|
|
|
|
if (promptDialog.cancelButton) {
|
|
if (promptDialog.cancelButton === true) {
|
|
buttons.push(localize('cancelButton', "Cancel"));
|
|
} else if (typeof promptDialog.cancelButton === 'string') {
|
|
buttons.push(promptDialog.cancelButton);
|
|
} else {
|
|
if (promptDialog.cancelButton.label) {
|
|
buttons.push(promptDialog.cancelButton.label);
|
|
} else {
|
|
buttons.push(localize('cancelButton', "Cancel"));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (buttons.length === 0) {
|
|
buttons.push(localize({ key: 'okButton', comment: ['&& denotes a mnemonic'] }, "&&OK"));
|
|
}
|
|
|
|
break;
|
|
}
|
|
case DialogKind.Input: {
|
|
const inputDialog = dialog as IInput;
|
|
|
|
if (inputDialog.primaryButton) {
|
|
buttons.push(inputDialog.primaryButton);
|
|
} else {
|
|
buttons.push(localize({ key: 'okButton', comment: ['&& denotes a mnemonic'] }, "&&OK"));
|
|
}
|
|
|
|
if (inputDialog.cancelButton) {
|
|
buttons.push(inputDialog.cancelButton);
|
|
} else {
|
|
buttons.push(localize('cancelButton', "Cancel"));
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return buttons;
|
|
}
|
|
|
|
protected getDialogType(type: Severity | DialogType | undefined): DialogType | undefined {
|
|
if (typeof type === 'string') {
|
|
return type;
|
|
}
|
|
|
|
if (typeof type === 'number') {
|
|
return (type === Severity.Info) ? 'info' : (type === Severity.Error) ? 'error' : (type === Severity.Warning) ? 'warning' : 'none';
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
protected getPromptResult<T>(prompt: IPrompt<T>, buttonIndex: number, checkboxChecked: boolean | undefined): IAsyncPromptResult<T> {
|
|
const promptButtons: IPromptBaseButton<T>[] = [...(prompt.buttons ?? [])];
|
|
if (prompt.cancelButton && typeof prompt.cancelButton !== 'string' && typeof prompt.cancelButton !== 'boolean') {
|
|
promptButtons.push(prompt.cancelButton);
|
|
}
|
|
|
|
let result = promptButtons[buttonIndex]?.run({ checkboxChecked });
|
|
if (!(result instanceof Promise)) {
|
|
result = Promise.resolve(result);
|
|
}
|
|
|
|
return { result, checkboxChecked };
|
|
}
|
|
|
|
abstract confirm(confirmation: IConfirmation): Promise<IConfirmationResult>;
|
|
abstract input(input: IInput): Promise<IInputResult>;
|
|
abstract prompt<T>(prompt: IPrompt<T>): Promise<IAsyncPromptResult<T>>;
|
|
abstract about(title: string, details: string, detailsToCopy: string): Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* A service to bring up modal dialogs.
|
|
*
|
|
* Note: use the `INotificationService.prompt()` method for a non-modal way to ask
|
|
* the user for input.
|
|
*/
|
|
export interface IDialogService {
|
|
|
|
readonly _serviceBrand: undefined;
|
|
|
|
/**
|
|
* An event that fires when a dialog is about to show.
|
|
*/
|
|
readonly onWillShowDialog: Event<void>;
|
|
|
|
/**
|
|
* An event that fires when a dialog did show (closed).
|
|
*/
|
|
readonly onDidShowDialog: Event<void>;
|
|
|
|
/**
|
|
* Ask the user for confirmation with a modal dialog.
|
|
*/
|
|
confirm(confirmation: IConfirmation): Promise<IConfirmationResult>;
|
|
|
|
/**
|
|
* Prompt the user with a modal dialog. Provides a bit
|
|
* more control over the dialog compared to the simpler
|
|
* `confirm` method. Specifically, allows to show more
|
|
* than 2 buttons and makes it easier to just show a
|
|
* message to the user.
|
|
*
|
|
* @returns a promise that resolves to the `T` result
|
|
* from the provided `IPromptButton<T>` or `undefined`.
|
|
*/
|
|
prompt<T>(prompt: IPromptWithCustomCancel<T>): Promise<IPromptResultWithCancel<T>>;
|
|
prompt<T>(prompt: IPromptWithDefaultCancel<T>): Promise<IPromptResult<T>>;
|
|
prompt<T>(prompt: IPrompt<T>): Promise<IPromptResult<T>>;
|
|
|
|
/**
|
|
* Present a modal dialog to the user asking for input.
|
|
*/
|
|
input(input: IInput): Promise<IInputResult>;
|
|
|
|
/**
|
|
* Show a modal info dialog.
|
|
*/
|
|
info(message: string, detail?: string): Promise<void>;
|
|
|
|
/**
|
|
* Show a modal warning dialog.
|
|
*/
|
|
warn(message: string, detail?: string): Promise<void>;
|
|
|
|
/**
|
|
* Show a modal error dialog.
|
|
*/
|
|
error(message: string, detail?: string): Promise<void>;
|
|
|
|
/**
|
|
* Present the about dialog to the user.
|
|
*/
|
|
about(): Promise<void>;
|
|
}
|
|
|
|
export const IFileDialogService = createDecorator<IFileDialogService>('fileDialogService');
|
|
|
|
/**
|
|
* A service to bring up file dialogs.
|
|
*/
|
|
export interface IFileDialogService {
|
|
|
|
readonly _serviceBrand: undefined;
|
|
|
|
/**
|
|
* The default path for a new file based on previously used files.
|
|
* @param schemeFilter The scheme of the file path. If no filter given, the scheme of the current window is used.
|
|
* Falls back to user home in the absence of enough information to find a better URI.
|
|
*/
|
|
defaultFilePath(schemeFilter?: string): Promise<URI>;
|
|
|
|
/**
|
|
* The default path for a new folder based on previously used folders.
|
|
* @param schemeFilter The scheme of the folder path. If no filter given, the scheme of the current window is used.
|
|
* Falls back to user home in the absence of enough information to find a better URI.
|
|
*/
|
|
defaultFolderPath(schemeFilter?: string): Promise<URI>;
|
|
|
|
/**
|
|
* The default path for a new workspace based on previously used workspaces.
|
|
* @param schemeFilter The scheme of the workspace path. If no filter given, the scheme of the current window is used.
|
|
* Falls back to user home in the absence of enough information to find a better URI.
|
|
*/
|
|
defaultWorkspacePath(schemeFilter?: string): Promise<URI>;
|
|
|
|
/**
|
|
* Shows a file-folder selection dialog and opens the selected entry.
|
|
*/
|
|
pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;
|
|
|
|
/**
|
|
* Shows a file selection dialog and opens the selected entry.
|
|
*/
|
|
pickFileAndOpen(options: IPickAndOpenOptions): Promise<void>;
|
|
|
|
/**
|
|
* Shows a folder selection dialog and opens the selected entry.
|
|
*/
|
|
pickFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;
|
|
|
|
/**
|
|
* Shows a workspace selection dialog and opens the selected entry.
|
|
*/
|
|
pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void>;
|
|
|
|
/**
|
|
* Shows a save file dialog and save the file at the chosen file URI.
|
|
*/
|
|
pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined>;
|
|
|
|
/**
|
|
* The preferred folder path to open the dialog at.
|
|
* @param schemeFilter The scheme of the file path. If no filter given, the scheme of the current window is used.
|
|
* Falls back to user home in the absence of a setting.
|
|
*/
|
|
preferredHome(schemeFilter?: string): Promise<URI>;
|
|
|
|
/**
|
|
* Shows a save file dialog and returns the chosen file URI.
|
|
*/
|
|
showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined>;
|
|
|
|
/**
|
|
* Shows a confirm dialog for saving 1-N files.
|
|
*/
|
|
showSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise<ConfirmResult>;
|
|
|
|
/**
|
|
* Shows a open file dialog and returns the chosen file URI.
|
|
*/
|
|
showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined>;
|
|
}
|
|
|
|
export const enum ConfirmResult {
|
|
SAVE,
|
|
DONT_SAVE,
|
|
CANCEL
|
|
}
|
|
|
|
const MAX_CONFIRM_FILES = 10;
|
|
export function getFileNamesMessage(fileNamesOrResources: readonly (string | URI)[]): string {
|
|
const message: string[] = [];
|
|
message.push(...fileNamesOrResources.slice(0, MAX_CONFIRM_FILES).map(fileNameOrResource => typeof fileNameOrResource === 'string' ? fileNameOrResource : basename(fileNameOrResource)));
|
|
|
|
if (fileNamesOrResources.length > MAX_CONFIRM_FILES) {
|
|
if (fileNamesOrResources.length - MAX_CONFIRM_FILES === 1) {
|
|
message.push(localize('moreFile', "...1 additional file not shown"));
|
|
} else {
|
|
message.push(localize('moreFiles', "...{0} additional files not shown", fileNamesOrResources.length - MAX_CONFIRM_FILES));
|
|
}
|
|
}
|
|
|
|
message.push('');
|
|
return message.join('\n');
|
|
}
|
|
|
|
export interface INativeOpenDialogOptions {
|
|
readonly forceNewWindow?: boolean;
|
|
|
|
readonly defaultPath?: string;
|
|
|
|
readonly telemetryEventName?: string;
|
|
readonly telemetryExtraData?: ITelemetryData;
|
|
}
|
|
|
|
export interface IMassagedMessageBoxOptions {
|
|
|
|
/**
|
|
* OS massaged message box options.
|
|
*/
|
|
readonly 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.
|
|
*/
|
|
readonly 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).withMnemonic);
|
|
let buttonIndeces = (options.buttons || []).map((button, index) => index);
|
|
|
|
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) {
|
|
const cancelButton = typeof cancelId === 'number' ? buttons[cancelId] : undefined;
|
|
|
|
if (isLinux || isMacintosh) {
|
|
|
|
// 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
|
|
|
|
// 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."
|
|
//
|
|
// However: it seems that older macOS versions where 3 buttons were presented in a row differ from this
|
|
// recommendation. In fact, cancel buttons were placed to the left of the default button and secondary
|
|
// buttons on the far left. To support these older macOS versions we have to manually shuffle the cancel
|
|
// button in the same way as we do on Linux. This will not have any impact on newer macOS versions where
|
|
// shuffling is done for us.
|
|
|
|
if (typeof cancelButton === 'string' && buttons.length > 1 && cancelId !== 1) {
|
|
buttons.splice(cancelId, 1);
|
|
buttons.splice(1, 0, cancelButton);
|
|
|
|
const cancelButtonIndex = buttonIndeces[cancelId];
|
|
buttonIndeces.splice(cancelId, 1);
|
|
buttonIndeces.splice(1, 0, cancelButtonIndex);
|
|
|
|
cancelId = 1;
|
|
}
|
|
|
|
if (isLinux && buttons.length > 1) {
|
|
buttons = buttons.reverse();
|
|
buttonIndeces = buttonIndeces.reverse();
|
|
|
|
defaultId = buttons.length - 1;
|
|
if (typeof cancelButton === 'string') {
|
|
cancelId = defaultId - 1;
|
|
}
|
|
}
|
|
} 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
|
|
|
|
if (typeof cancelButton === 'string' && buttons.length > 1 && cancelId !== buttons.length - 1 /* last action */) {
|
|
buttons.splice(cancelId, 1);
|
|
buttons.push(cancelButton);
|
|
|
|
const buttonIndex = buttonIndeces[cancelId];
|
|
buttonIndeces.splice(cancelId, 1);
|
|
buttonIndeces.push(buttonIndex);
|
|
|
|
cancelId = buttons.length - 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
massagedOptions.buttons = buttons;
|
|
massagedOptions.defaultId = defaultId;
|
|
massagedOptions.cancelId = cancelId;
|
|
massagedOptions.noLink = true;
|
|
massagedOptions.title = massagedOptions.title || productService.nameLong;
|
|
|
|
return {
|
|
options: massagedOptions,
|
|
buttonIndeces
|
|
};
|
|
}
|