dialog.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import { app, BrowserWindow } from 'electron/main';
  2. import type { OpenDialogOptions, OpenDialogReturnValue, MessageBoxOptions, SaveDialogOptions, SaveDialogReturnValue, MessageBoxReturnValue, CertificateTrustDialogOptions } from 'electron/main';
  3. const dialogBinding = process._linkedBinding('electron_browser_dialog');
  4. const DialogType = {
  5. OPEN: 'OPEN' as 'OPEN',
  6. SAVE: 'SAVE' as 'SAVE'
  7. };
  8. enum SaveFileDialogProperties {
  9. createDirectory = 1 << 0,
  10. showHiddenFiles = 1 << 1,
  11. treatPackageAsDirectory = 1 << 2,
  12. showOverwriteConfirmation = 1 << 3,
  13. dontAddToRecent = 1 << 4
  14. }
  15. enum OpenFileDialogProperties {
  16. openFile = 1 << 0,
  17. openDirectory = 1 << 1,
  18. multiSelections = 1 << 2,
  19. createDirectory = 1 << 3, // macOS
  20. showHiddenFiles = 1 << 4,
  21. promptToCreate = 1 << 5, // Windows
  22. noResolveAliases = 1 << 6, // macOS
  23. treatPackageAsDirectory = 1 << 7, // macOS
  24. dontAddToRecent = 1 << 8 // Windows
  25. }
  26. const normalizeAccessKey = (text: string) => {
  27. if (typeof text !== 'string') return text;
  28. // macOS does not have access keys so remove single ampersands
  29. // and replace double ampersands with a single ampersand
  30. if (process.platform === 'darwin') {
  31. return text.replace(/&(&?)/g, '$1');
  32. }
  33. // Linux uses a single underscore as an access key prefix so escape
  34. // existing single underscores with a second underscore, replace double
  35. // ampersands with a single ampersand, and replace a single ampersand with
  36. // a single underscore
  37. if (process.platform === 'linux') {
  38. return text.replace(/_/g, '__').replace(/&(.?)/g, (match, after) => {
  39. if (after === '&') return after;
  40. return `_${after}`;
  41. });
  42. }
  43. return text;
  44. };
  45. const checkAppInitialized = function () {
  46. if (!app.isReady()) {
  47. throw new Error('dialog module can only be used after app is ready');
  48. }
  49. };
  50. const setupOpenDialogProperties = (properties: (keyof typeof OpenFileDialogProperties)[]): number => {
  51. let dialogProperties = 0;
  52. for (const property of properties) {
  53. if (Object.prototype.hasOwnProperty.call(OpenFileDialogProperties, property)) { dialogProperties |= OpenFileDialogProperties[property]; }
  54. }
  55. return dialogProperties;
  56. };
  57. const setupSaveDialogProperties = (properties: (keyof typeof SaveFileDialogProperties)[]): number => {
  58. let dialogProperties = 0;
  59. for (const property of properties) {
  60. if (Object.prototype.hasOwnProperty.call(SaveFileDialogProperties, property)) { dialogProperties |= SaveFileDialogProperties[property]; }
  61. }
  62. return dialogProperties;
  63. };
  64. const setupDialogProperties = (type: keyof typeof DialogType, properties: string[]): number => {
  65. if (type === DialogType.OPEN) {
  66. return setupOpenDialogProperties(properties as (keyof typeof OpenFileDialogProperties)[]);
  67. } else if (type === DialogType.SAVE) {
  68. return setupSaveDialogProperties(properties as (keyof typeof SaveFileDialogProperties)[]);
  69. } else {
  70. return 0;
  71. }
  72. };
  73. const saveDialog = (sync: boolean, window: BrowserWindow | null, options?: SaveDialogOptions) => {
  74. checkAppInitialized();
  75. if (options == null) options = { title: 'Save' };
  76. const {
  77. buttonLabel = '',
  78. defaultPath = '',
  79. filters = [],
  80. properties = [],
  81. title = '',
  82. message = '',
  83. securityScopedBookmarks = false,
  84. nameFieldLabel = '',
  85. showsTagField = true
  86. } = options;
  87. if (typeof title !== 'string') throw new TypeError('Title must be a string');
  88. if (typeof buttonLabel !== 'string') throw new TypeError('Button label must be a string');
  89. if (typeof defaultPath !== 'string') throw new TypeError('Default path must be a string');
  90. if (typeof message !== 'string') throw new TypeError('Message must be a string');
  91. if (typeof nameFieldLabel !== 'string') throw new TypeError('Name field label must be a string');
  92. const settings = {
  93. buttonLabel,
  94. defaultPath,
  95. filters,
  96. title,
  97. message,
  98. securityScopedBookmarks,
  99. nameFieldLabel,
  100. showsTagField,
  101. window,
  102. properties: setupDialogProperties(DialogType.SAVE, properties)
  103. };
  104. return sync ? dialogBinding.showSaveDialogSync(settings) : dialogBinding.showSaveDialog(settings);
  105. };
  106. const openDialog = (sync: boolean, window: BrowserWindow | null, options?: OpenDialogOptions) => {
  107. checkAppInitialized();
  108. if (options == null) {
  109. options = {
  110. title: 'Open',
  111. properties: ['openFile']
  112. };
  113. }
  114. const {
  115. buttonLabel = '',
  116. defaultPath = '',
  117. filters = [],
  118. properties = ['openFile'],
  119. title = '',
  120. message = '',
  121. securityScopedBookmarks = false
  122. } = options;
  123. if (!Array.isArray(properties)) throw new TypeError('Properties must be an array');
  124. if (typeof title !== 'string') throw new TypeError('Title must be a string');
  125. if (typeof buttonLabel !== 'string') throw new TypeError('Button label must be a string');
  126. if (typeof defaultPath !== 'string') throw new TypeError('Default path must be a string');
  127. if (typeof message !== 'string') throw new TypeError('Message must be a string');
  128. const settings = {
  129. title,
  130. buttonLabel,
  131. defaultPath,
  132. filters,
  133. message,
  134. securityScopedBookmarks,
  135. window,
  136. properties: setupDialogProperties(DialogType.OPEN, properties)
  137. };
  138. return (sync) ? dialogBinding.showOpenDialogSync(settings) : dialogBinding.showOpenDialog(settings);
  139. };
  140. const messageBox = (sync: boolean, window: BrowserWindow | null, options?: MessageBoxOptions) => {
  141. checkAppInitialized();
  142. if (options == null) options = { type: 'none', message: '' };
  143. const messageBoxTypes = ['none', 'info', 'warning', 'error', 'question'];
  144. let {
  145. buttons = [],
  146. cancelId,
  147. checkboxLabel = '',
  148. checkboxChecked,
  149. defaultId = -1,
  150. detail = '',
  151. icon = null,
  152. noLink = false,
  153. message = '',
  154. title = '',
  155. type = 'none'
  156. } = options;
  157. const messageBoxType = messageBoxTypes.indexOf(type);
  158. if (messageBoxType === -1) throw new TypeError('Invalid message box type');
  159. if (!Array.isArray(buttons)) throw new TypeError('Buttons must be an array');
  160. if (options.normalizeAccessKeys) buttons = buttons.map(normalizeAccessKey);
  161. if (typeof title !== 'string') throw new TypeError('Title must be a string');
  162. if (typeof noLink !== 'boolean') throw new TypeError('noLink must be a boolean');
  163. if (typeof message !== 'string') throw new TypeError('Message must be a string');
  164. if (typeof detail !== 'string') throw new TypeError('Detail must be a string');
  165. if (typeof checkboxLabel !== 'string') throw new TypeError('checkboxLabel must be a string');
  166. checkboxChecked = !!checkboxChecked;
  167. if (checkboxChecked && !checkboxLabel) {
  168. throw new Error('checkboxChecked requires that checkboxLabel also be passed');
  169. }
  170. // Choose a default button to get selected when dialog is cancelled.
  171. if (cancelId == null) {
  172. // If the defaultId is set to 0, ensure the cancel button is a different index (1)
  173. cancelId = (defaultId === 0 && buttons.length > 1) ? 1 : 0;
  174. for (let i = 0; i < buttons.length; i++) {
  175. const text = buttons[i].toLowerCase();
  176. if (text === 'cancel' || text === 'no') {
  177. cancelId = i;
  178. break;
  179. }
  180. }
  181. }
  182. const settings = {
  183. window,
  184. messageBoxType,
  185. buttons,
  186. defaultId,
  187. cancelId,
  188. noLink,
  189. title,
  190. message,
  191. detail,
  192. checkboxLabel,
  193. checkboxChecked,
  194. icon
  195. };
  196. if (sync) {
  197. return dialogBinding.showMessageBoxSync(settings);
  198. } else {
  199. return dialogBinding.showMessageBox(settings);
  200. }
  201. };
  202. // eat dirt, eslint
  203. /* eslint-disable import/export */
  204. export function showOpenDialog(window: BrowserWindow, options: OpenDialogOptions): OpenDialogReturnValue;
  205. export function showOpenDialog(options: OpenDialogOptions): OpenDialogReturnValue;
  206. export function showOpenDialog (windowOrOptions: BrowserWindow | OpenDialogOptions, maybeOptions?: OpenDialogOptions): OpenDialogReturnValue {
  207. const window = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? null : windowOrOptions);
  208. const options = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? windowOrOptions : maybeOptions);
  209. return openDialog(false, window, options);
  210. }
  211. export function showOpenDialogSync(window: BrowserWindow, options: OpenDialogOptions): OpenDialogReturnValue;
  212. export function showOpenDialogSync(options: OpenDialogOptions): OpenDialogReturnValue;
  213. export function showOpenDialogSync (windowOrOptions: BrowserWindow | OpenDialogOptions, maybeOptions?: OpenDialogOptions): OpenDialogReturnValue {
  214. const window = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? null : windowOrOptions);
  215. const options = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? windowOrOptions : maybeOptions);
  216. return openDialog(true, window, options);
  217. }
  218. export function showSaveDialog(window: BrowserWindow, options: SaveDialogOptions): SaveDialogReturnValue;
  219. export function showSaveDialog(options: SaveDialogOptions): SaveDialogReturnValue;
  220. export function showSaveDialog (windowOrOptions: BrowserWindow | SaveDialogOptions, maybeOptions?: SaveDialogOptions): SaveDialogReturnValue {
  221. const window = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? null : windowOrOptions);
  222. const options = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? windowOrOptions : maybeOptions);
  223. return saveDialog(false, window, options);
  224. }
  225. export function showSaveDialogSync(window: BrowserWindow, options: SaveDialogOptions): SaveDialogReturnValue;
  226. export function showSaveDialogSync(options: SaveDialogOptions): SaveDialogReturnValue;
  227. export function showSaveDialogSync (windowOrOptions: BrowserWindow | SaveDialogOptions, maybeOptions?: SaveDialogOptions): SaveDialogReturnValue {
  228. const window = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? null : windowOrOptions);
  229. const options = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? windowOrOptions : maybeOptions);
  230. return saveDialog(true, window, options);
  231. }
  232. export function showMessageBox(window: BrowserWindow, options: MessageBoxOptions): MessageBoxReturnValue;
  233. export function showMessageBox(options: MessageBoxOptions): MessageBoxReturnValue;
  234. export function showMessageBox (windowOrOptions: BrowserWindow | MessageBoxOptions, maybeOptions?: MessageBoxOptions): MessageBoxReturnValue {
  235. const window = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? null : windowOrOptions);
  236. const options = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? windowOrOptions : maybeOptions);
  237. return messageBox(false, window, options);
  238. }
  239. export function showMessageBoxSync(window: BrowserWindow, options: MessageBoxOptions): MessageBoxReturnValue;
  240. export function showMessageBoxSync(options: MessageBoxOptions): MessageBoxReturnValue;
  241. export function showMessageBoxSync (windowOrOptions: BrowserWindow | MessageBoxOptions, maybeOptions?: MessageBoxOptions): MessageBoxReturnValue {
  242. const window = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? null : windowOrOptions);
  243. const options = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? windowOrOptions : maybeOptions);
  244. return messageBox(true, window, options);
  245. }
  246. export function showErrorBox (...args: any[]) {
  247. return dialogBinding.showErrorBox(...args);
  248. }
  249. export function showCertificateTrustDialog (windowOrOptions: BrowserWindow | CertificateTrustDialogOptions, maybeOptions?: CertificateTrustDialogOptions) {
  250. const window = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? null : windowOrOptions);
  251. const options = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? windowOrOptions : maybeOptions);
  252. if (options == null || typeof options !== 'object') {
  253. throw new TypeError('options must be an object');
  254. }
  255. const { certificate, message = '' } = options;
  256. if (certificate == null || typeof certificate !== 'object') {
  257. throw new TypeError('certificate must be an object');
  258. }
  259. if (typeof message !== 'string') throw new TypeError('message must be a string');
  260. return dialogBinding.showCertificateTrustDialog(window, certificate, message);
  261. }