From d6f90bdd92eaa667a9e4b25a03bc7c91cc8a475c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr> Date: Tue, 7 Jul 2020 15:01:14 +0200 Subject: [PATCH] [dome] TS dialogs --- ivette/src/dome/src/renderer/dialogs.js | 307 ----------------------- ivette/src/dome/src/renderer/dialogs.tsx | 278 ++++++++++++++++++++ 2 files changed, 278 insertions(+), 307 deletions(-) delete mode 100644 ivette/src/dome/src/renderer/dialogs.js create mode 100644 ivette/src/dome/src/renderer/dialogs.tsx diff --git a/ivette/src/dome/src/renderer/dialogs.js b/ivette/src/dome/src/renderer/dialogs.js deleted file mode 100644 index 1532f867e03..00000000000 --- a/ivette/src/dome/src/renderer/dialogs.js +++ /dev/null @@ -1,307 +0,0 @@ -/** - @packageDocumentation - @module dome/dialogs - @description - Various kind of (modal) dialogs attached to the main application window. - */ - -import filepath from 'path' ; -import { remote } from 'electron' ; -import * as System from 'dome/system' ; - -// -------------------------------------------------------------------------- -// --- Message Box -// -------------------------------------------------------------------------- - -const defaultItems = [ - { value:undefined }, - { value:true, label:'Ok' } -]; - -const valueLabel = (v) => { - switch(v) { - case undefined: return 'Cancel' ; - case true: return 'Ok' ; - case false: return 'No' ; - default: return ''+v ; - } -}; - -const itemLabel = ({value,label}) => label || valueLabel(value) ; -const isDefault = ({value,label}) => value===true || label==='Ok' || label==='Yes' ; -const isCancel = ({value,label}) => !value || label==='Cancel' || label==='No' ; - -/** - @summary Show a configurable message box. - @parameter {object} options - configuration (see above) - @return {Promise} the selected option (see above) - @description -The available fields and options for configuring the dialog are: - -| Options | Type | Description | -|:--------|:----:|:------------| -| `kind` | `'none','info','error','warning'` | Icon of the message box | -| `title` | `string` (_opt._) | Heading of message box | -| `message` | `string` | Message text | -| `buttons` | `button[]` (_opt._) | Dialog buttons | -| `defaultValue` | (any) (_opt._) | Value of the default button | -| `cancelValue` | (any) (_opt._) | Value of the cancel key | - -The dialog buttons are specified by objects with the following fields: - -| Button Field | Type | Description | -|:-------------|:----:|:------------| -| `label` | `string` | Button label | -| `value` | (any) | Button identifier (items only) | - -The returned promise object is never rejected, and is asynchronously -resolved into: -- the cancel value if the cancel key is pressed, -- the default value if the enter key is pressed, -- or the value of the clicked button otherwised. - -The default buttons are `"Ok"` and `"Cancel"` associated to values `true` and -`undefined`, which are also associated to the enter and cancel keys. -Unless specified, the default value is associated with the first button -with 'true' value or 'Ok' or 'Yes' label, -and the cancel value is the first button with a falsy value or 'Cancel' -or 'No' label. -*/ -export function showMessageBox( options ) -{ - const { - kind, - title, - message, - defaultValue, - cancelValue, - buttons = defaultItems - } = options ; - - const labels = buttons.map(itemLabel); - let defaultId = - defaultValue === undefined - ? buttons.findIndex(isDefault) - : buttons.findIndex((a) => a.value === defaultValue); - let cancelId = - cancelValue === undefined - ? buttons.findIndex(isCancel) - : buttons.findIndex((a) => a.value === cancelValue); - - if (cancelId === defaultId) cancelId = -1; - - return remote.dialog.showMessageBox( - remote.getCurrentWindow(), - { - type:kind, - message: message && title, - detail: message || title, - defaultId, cancelId, buttons: labels - } - ).then((result) => { - let itemIndex = result ? result.response : -1 ; - return itemIndex ? buttons[itemIndex].value : cancelValue ; - }); -} - -// -------------------------------------------------------------------------- -// --- openFile dialog -// -------------------------------------------------------------------------- - -const defaultPath = (path) => filepath.extname(path) ? filepath.dirname(path) : path ; - -/** - @summary Show a dialog for opening file. - @parameter {object} options - configuration (see above) - @return {Promise} the selected file (see above) - @description -The available fields and options for configuring the dialog are: - -| Options | Type | Description | -|:--------|:----:|:------------| -| `message` | `string` (_opt._) | Prompt message | -| `label` | `string` (_opt._) | Open button label | -| `path` | `filepath` (_opt._) | Initially selected path | -| `hidden` | `boolean` (_opt._) | Show hidden files (not by default) | -| `filters` | `filter[]` (_opt._) | File filters (all files by default) | - -The file filters are object with the following fields: - -| Filter Field | Type | Description | -|:-------------|:----:|:------------| -| `name` | `string` | Filter name | -| `extensions` | `string[]` | Allowed file extensions, _without_ dots («.») | - -A file filter with `extensions:["*"]` would accept any file extension. - -The returned promise object will be asynchronously: -- either _resolved_ with `undefined` if no file has been selected, -- or _resolved_ with the selected path - -The promise is never rejected. - -*/ -export function showOpenFile( options ) -{ - const { message, label, path, hidden, filters } = options ; - const properties = [ 'openFile' ]; - if (hidden) properties.push('showHiddenFiles'); - - return remote.dialog.showOpenDialog( - remote.getCurrentWindow(), - { - message, buttonLabel: label, - defaultPath: path && defaultPath(path), - properties, filters - } - ).then(result => { - if (!result.canceled && result.filePaths && result.filePaths.length > 0) - return result.filePaths[0] ; - else - return undefined ; - }); -} - -/** - @summary Show a dialog for opening file. - @parameter {object} options - configuration (see above) - @return {Promise} the selected file(s) (see above) - @description -The available fields and options for configuring the dialog are: - -| Options | Type | Description | -|:--------|:----:|:------------| -| `message` | `string` (_opt._) | Prompt message | -| `label` | `string` (_opt._) | Open button label | -| `path` | `filepath` (_opt._) | Initially selected path | -| `hidden` | `boolean` (_opt._) | Show hidden files (not by default) | -| `filters` | `filter[]` (_opt._) | File filters (all files by default) | - -The file filters are object with the following fields: - -| Filter Field | Type | Description | -|:-------------|:----:|:------------| -| `name` | `string` | Filter name | -| `extensions` | `string[]` | Allowed file extensions, _without_ dots («.») | - -A file filter with `extensions:["*"]` would accept any file extension. - -The returned promise object will be asynchronously: -- either _resolved_ with `undefined` if no file has been selected, -- or _resolved_ with the selected paths - -The promise is never rejected. - -*/ -export function showOpenFiles( options ) -{ - const { message, label, path, hidden, filters } = options ; - const properties = [ 'openFile', 'multiSelections' ]; - if (hidden) properties.push('showHiddenFiles'); - - return remote.dialog.showOpenDialog( - remote.getCurrentWindow(), - { - message, buttonLabel: label, - defaultPath: path && defaultPath(path), - properties, filters - } - ).then(result => { - if (!result.canceled && result.filePaths && result.filePaths.length > 0) - return result.filePaths ; - else - return undefined ; - }); -} - - -// -------------------------------------------------------------------------- -// --- saveFile dialog -// -------------------------------------------------------------------------- - -/** - @summary Show a dialog for saving file. - @parameter {object} options - configuration (see above) - @return {Promise} the selected path (see above) - @description -The available fields and options for configuring the dialog are: - -| Options | Type | Description | -|:--------|:----:|:------------| -| `message` | `string` (_opt._) | Prompt message | -| `label` | `string` (_opt._) | Save button label | -| `path` | `filepath` (_opt._) | Initially selected path | -| `filters` | `filter[]` (_opt._) | Cf. `openFileDialog` | - -The returned promise object will be asynchronously: -- either _resolved_ with `undefined` if no file has been selected, -- or _resolved_ with the selected (single) path. - -The promise is never rejected. - -*/ -export function showSaveFile( options ) -{ - const { message, label, path, filters } = options ; - - return remote.dialog.showSaveDialog( - remote.getCurrentWindow(), - { - message, buttonLabel: label, - defaultPath: path, - filters - } - ); -} - -// -------------------------------------------------------------------------- -// --- openDir dialog -// -------------------------------------------------------------------------- - -/** - @summary Show a dialog for selecting directories. - @parameter {object} options - configuration (see above) - @return {Promise} the selected directories (see above) - @description -The available fields and options for configuring the dialog are: - -| Options | Type | Description | -|:--------|:----:|:------------| -| `message` | `string` (_opt._) | Prompt message | -| `label` | `string` (_opt._) | Open button label | -| `path` | `filepath` (_opt._) | Initially selected path | -| `hidden` | `boolean` (_opt._) | Show hidden files (not by default) | - -The returned promise object will be asynchronously: -- either _resolved_ with `undefined` if no file has been selected, -- or _resolved_ with the selected path - -*/ -export function showOpenDir( options ) -{ - const { message, label, path, hidden } = options ; - const properties = [ 'openDirectory' ]; - if (hidden) properties.push('showHiddenFiles'); - - switch(System.platform) { - case 'macos': properties.push( 'createDirectory' ); break; - case 'windows': properties.push( 'promptToCreate' ); break; - } - - return remote.dialog.showOpenDialog( - remote.getCurrentWindow(), - { - message, - buttonLabel: label, - defaultPath: path, - properties - } - ).then(result => { - if (!result.canceled && result.filePaths && result.filePaths.length > 0) - return result.filePaths[0] ; - else - return undefined ; - }); -} - -// -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/dialogs.tsx b/ivette/src/dome/src/renderer/dialogs.tsx new file mode 100644 index 00000000000..b4871cc69e9 --- /dev/null +++ b/ivette/src/dome/src/renderer/dialogs.tsx @@ -0,0 +1,278 @@ +/** + Various kind of (modal) dialogs attached to the main application window. + @packageDocumentation + @module dome/dialogs + */ + +import filepath from 'path'; +import { remote } from 'electron'; +import * as System from 'dome/system'; + +// -------------------------------------------------------------------------- +// --- Message Box +// -------------------------------------------------------------------------- + +export interface DialogButton<A> { + label?: string; + value?: A; +}; + +const defaultItems: DialogButton<boolean>[] = [ + { value: undefined }, + { value: true, label: 'Ok' } +]; + +const valueLabel = (v: any) => { + switch (v) { + case undefined: return 'Cancel'; + case true: return 'Ok'; + case false: return 'No'; + default: return '' + v; + } +}; + +const itemLabel = ({ value, label }: DialogButton<any>) => + (label || valueLabel(value)); + +const isDefault = ({ value, label }: DialogButton<any>) => + (value === true || label === 'Ok' || label === 'Yes'); + +const isCancel = ({ value, label }: DialogButton<any>) => + (!value || label === 'Cancel' || label === 'No'); + + +export type MessageKind = 'none' | 'info' | 'error' | 'warning'; + +export interface MessageProps<A> { + /** Dialog window icon (default is `'none'`. */ + kind?: MessageKind; + /** Message text (short sentence). */ + message: string; + /** Message details (short sentence). */ + details?: string; + /** Message buttons.*/ + buttons?: DialogButton<A>[]; + /** Default button's value. */ + defaultValue?: A; + /** Cancel value. */ + cancelValue?: A; +} + +/** + Show a configurable message box. + + The returned promise object is never rejected, and is asynchronously + resolved into: + - the cancel value if the cancel key is pressed, + - the default value if the enter key is pressed, + - or the value of the clicked button otherwised. + + The default buttons are `"Ok"` and `"Cancel"` associated to values `true` and + `undefined`, which are also associated to the enter and cancel keys. + Unless specified, the default value is associated with the first button + with 'true' value or 'Ok' or 'Yes' label, + and the cancel value is the first button with a falsy value or 'Cancel' + or 'No' label. + */ +export async function showMessageBox<A>( + props: MessageProps<A>, +): Promise<A | boolean | undefined> { + const { + kind, + message, + details, + defaultValue, + cancelValue, + buttons = (defaultItems as DialogButton<A | boolean>[]) + } = props; + + const labels = buttons.map(itemLabel); + let defaultId = + defaultValue === undefined + ? buttons.findIndex(isDefault) + : buttons.findIndex((a) => a.value === defaultValue); + let cancelId = + cancelValue === undefined + ? buttons.findIndex(isCancel) + : buttons.findIndex((a) => a.value === cancelValue); + + if (cancelId === defaultId) cancelId = -1; + + return remote.dialog.showMessageBox( + remote.getCurrentWindow(), + { + type: kind, message, + detail: details, defaultId, cancelId, buttons: labels + } + ).then((result) => { + let itemIndex = result ? result.response : -1; + return itemIndex ? buttons[itemIndex].value : cancelValue; + }); +} + +// -------------------------------------------------------------------------- +// --- File Dialogs +// -------------------------------------------------------------------------- + +const defaultPath = + (path: string) => filepath.extname(path) ? filepath.dirname(path) : path; + +export interface FileFilter { + /** Filter name. */ + name: string; + /** + Allowed extensions, _without_ dot. + Use `['*']` to accept all files. + */ + extensions: string[]; +} + +export interface FileDialogProps { + /** Prompt message. */ + message?: string; + /** Open button label (default is « Open »). */ + label?: string; + /** Initially selected path. */ + path?: string; +} + +export interface SaveFileProps extends FileDialogProps { + /** File filters (default to all). */ + filters?: FileFilter[]; +} + +export interface OpenFileProps extends SaveFileProps { + /** Show hidden files (default is `false`). */ + hidden?: boolean; +} + +export interface OpenDirProps extends FileDialogProps { + /** Show hidden directories (default is `false`). */ + hidden?: boolean; +} + +// -------------------------------------------------------------------------- +// --- openFile dialog +// -------------------------------------------------------------------------- + +/** + Show a dialog for opening file. + A file filter with `extensions:["*"]` would accept any file extension. + + The returned promise object will be asynchronously: + - either _resolved_ with `undefined` if no file has been selected, + - or _resolved_ with the selected path + + The promise is never rejected. + */ +export async function showOpenFile(props: OpenFileProps): Promise<string | undefined> { + const { message, label, path, hidden = false, filters } = props; + return remote.dialog.showOpenDialog( + remote.getCurrentWindow(), + { + message, buttonLabel: label, + defaultPath: path && defaultPath(path), + properties: (hidden ? ['openFile', 'showHiddenFiles'] : ['openFile']), + filters, + } + ).then(result => { + if (!result.canceled && result.filePaths && result.filePaths.length > 0) + return result.filePaths[0]; + else + return undefined; + }); +} + +/** + Show a dialog for opening files multiple files. +*/ +export async function showOpenFiles(props: OpenFileProps): Promise<string[] | undefined> { + const { message, label, path, hidden, filters } = props; + + return remote.dialog.showOpenDialog( + remote.getCurrentWindow(), + { + message, buttonLabel: label, + defaultPath: path && defaultPath(path), + properties: ( + hidden + ? ['openFile', 'multiSelections', 'showHiddenFiles'] + : ['openFile', 'multiSelections'] + ), + filters, + } + ).then(result => { + if (!result.canceled && result.filePaths && result.filePaths.length > 0) + return result.filePaths; + else + return undefined; + }); +} + + +// -------------------------------------------------------------------------- +// --- saveFile dialog +// -------------------------------------------------------------------------- + +/** + Show a dialog for saving file. + + The returned promise object will be asynchronously: + - either _resolved_ with `undefined` when canceled, + - or _resolved_ with the selected (single) path. + + The promise is never rejected. +*/ +export async function showSaveFile( + props: SaveFileProps, +): Promise<string | undefined> { + const { message, label, path, filters } = props; + return remote.dialog.showSaveDialog( + remote.getCurrentWindow(), + { + message, buttonLabel: label, + defaultPath: path, + filters + } + ).then(({ canceled, filePath }) => canceled ? undefined : filePath); +} + +// -------------------------------------------------------------------------- +// --- openDir dialog +// -------------------------------------------------------------------------- + +type openDirProperty = + 'openDirectory' | 'showHiddenFiles' | 'createDirectory' | 'promptToCreate'; + +/** + Show a dialog for selecting directories. + */ +export async function showOpenDir( + props: OpenDirProps, +): Promise<string | undefined> { + const { message, label, path, hidden } = props; + const properties: openDirProperty[] = ['openDirectory']; + if (hidden) properties.push('showHiddenFiles'); + + switch (System.platform) { + case 'macos': properties.push('createDirectory'); break; + case 'windows': properties.push('promptToCreate'); break; + } + + return remote.dialog.showOpenDialog( + remote.getCurrentWindow(), + { + message, + buttonLabel: label, + defaultPath: path, + properties + } + ).then(result => { + if (!result.canceled && result.filePaths && result.filePaths.length > 0) + return result.filePaths[0]; + else + return undefined; + }); +} + +// -------------------------------------------------------------------------- -- GitLab